A registry lookup answers a question you already know to ask. A webhook answers a different question: did anything relevant change in the aircraft records my software already tracks?
That distinction matters if you're building maintenance software, flight-school tooling, a broker's inventory system, or an internal fleet database. Polling ten known registrations is manageable. Polling them every few minutes is mostly repetitive work. Running your own FAA registry download and snapshot comparison can work too, but then you're responsible for ingestion, filtering, update timing, and failure recovery.
The Grizzly Bulls Aircraft change feed supports bounded reads on Free. Paid watches add push delivery: you register the N-numbers your application cares about and receive signed notifications when reviewed changes match. This guide focuses on the receiving side, which you can test locally without buying a plan or receiving a real FAA event.
First, distinguish registry changes from aircraft events
These are registry notifications. They do not track an aircraft's position, detect a flight, or establish that title legally transferred at a specific moment.
The FAA releasable aircraft database is the upstream source. When successive accepted source snapshots differ, Grizzly Bulls can publish a reviewed change with its observation timestamp. That timestamp tells you when the change was observed in the source, not necessarily when an underlying transaction became legally effective.
A receiver should therefore treat incoming data as an update to check or review. It's useful to flag a changed registration status in an intake queue. It's not enough, on its own, to declare the aircraft legally available, sold, or owned by a particular party.
Run a real receiver with a deliberately fake event
We've published a dependency-free Node.js webhook receiver alongside our open-source Aircraft Registry Explorer. It verifies the published signing contract and writes minimal local receipts. You can examine its receiver implementation rather than copying an incomplete HMAC snippet from a blog post.
Install Node.js 24, clone the repository, then start the example from its root. In your first terminal:
1git clone https://github.com/Grizzly-Bulls/aircraft-registry-explorer.git
2cd aircraft-registry-explorer
3export AIRCRAFT_WEBHOOK_WATCH_ID='aw_aaaaaaaaaaaaaaaaaaaaaaaa'
4export AIRCRAFT_WEBHOOK_SIGNING_SECRET="whsec_$(node -e 'process.stdout.write("A".repeat(43))')"
5node examples/aircraft-webhook-receiver/server.mjsIn a second terminal in the same repository, export the same two synthetic values and send the local test:
1export AIRCRAFT_WEBHOOK_WATCH_ID='aw_aaaaaaaaaaaaaaaaaaaaaaaa'
2export AIRCRAFT_WEBHOOK_SIGNING_SECRET="whsec_$(node -e 'process.stdout.write("A".repeat(43))')"
3node examples/aircraft-webhook-receiver/send-test.mjsThe expected response is Local synthetic webhook: HTTP 200 recorded. The receiver should log a sanitized receipt. Its default listener is 127.0.0.1:8787, not a public endpoint.
Nothing here calls Grizzly Bulls' API, starts a subscription, or claims a real registration change occurred. The fixed secret is for local testing only. Never deploy it or reuse it for a real watch. The example's aircraft.webhook.test message is intentionally synthetic.
You can also run the offline tests:
1node --test examples/aircraft-webhook-receiver/receiver.test.mjsWhat exactly gets signed?
The sender uses a watch-specific signing secret and computes HMAC-SHA256 over the Unix timestamp, a period, and the original request body bytes.
1X-Grizzly-Bulls-Webhook-Timestamp: <unix-seconds>
2X-Grizzly-Bulls-Event-Id: <public-event-id>
3X-Grizzly-Bulls-Delivery-Id: <delivery-id>
4X-Grizzly-Bulls-Delivery-Attempt: <attempt-number>
5X-Grizzly-Bulls-Signature: v1=<lowercase-hex-hmac-sha256>
6
7HMAC key: signingSecret
8HMAC input: timestamp + "." + rawBodyTwo mistakes are easy to make here.
First, don't parse JSON and then stringify it before checking the signature. A different whitespace character or field order changes the bytes. Your HTTP framework must preserve the raw request body for verification.
Second, don't compare signature strings with an ordinary equality operator. Decode the hex digest, ensure its shape is valid, and compare equal-length byte buffers in constant time. The tested example uses Node's createHmac and timingSafeEqual.
The receiver also checks that the timestamp is within five minutes of its clock, the event and delivery headers are well-formed, the payload's event ID matches its header, and the watch ID is the configured one. Only after that verification does it parse the event for processing and write a receipt. Keep the server clock synchronized; an otherwise genuine request with a stale timestamp is rejected.
Retries make idempotency part of the integration
HTTP delivery cannot guarantee that the sender knows you processed an event. Imagine your database write succeeds but the connection drops before the 200 response reaches the sender. A retry can legitimately arrive with a new delivery attempt for the same logical event.
Deduplicate using the watch ID and event ID, not the delivery attempt number. The local example makes a small receipt marker keyed by that pair and acknowledges an already-recorded event without writing a second receipt.
That demonstrates the idea, not production exactly-once processing. If a notification updates customer records, sends an email, or starts maintenance work, the receipt and resulting business transaction need their own durable, transactional protection. Two receiver instances should not be able to perform the business effect twice.
The example returns 401 for invalid signatures and malformed signed requests, 415 for unsupported content types, and 503 when it cannot persist a receipt. Don't acknowledge a critical update with 200 before the part of your system responsible for it has durably accepted it. A temporary failure is better handled by the sender's bounded retry policy than silently losing work.
The production endpoint is different from localhost
When you're ready for actual delivery, host the receiver behind a public HTTPS endpoint on port 443. The Aircraft API rejects localhost, private IP addresses, plaintext callbacks, and other ports for watch delivery. The example's plain HTTP server is designed to sit behind your TLS-terminating reverse proxy, not be exposed to the public Internet.
Your proxy must forward the raw body and the X-Grizzly-Bulls-* headers intact. Constrain its route to POST /webhooks/aircraft, cap incoming payload size, protect the health endpoint, and keep signing keys and receipts outside any publicly served directory. The example accepts up to 2 MiB; your deployment should preserve that bound or apply a stricter documented one.
Use real logging and monitoring for failures, but not full payload dumps or authentication headers. The sample prints minimal non-secret event information. Registration payloads may change and current FAA releasability rules can suppress personal information, so there is little reason to hoard entire callback bodies for debugging.
Connect a paid watch only when it saves real work
Free includes bounded reads of current registrations, history, and observed changes, but not live watch delivery. You can evaluate whether the change feed has the evidence you need before paying for push notifications.
When a real application needs notifications for known N-numbers, Developer ($49/month) or Business ($199/month) provides paid watch capacity. From a trusted server or terminal, create a watch with an HTTPS callback:
1curl --fail-with-body \
2 -X POST 'https://api.grizzlybulls.com/v1/aircraft/watch-lists' \
3 -H "Authorization: Bearer $GRIZZLY_BULLS_API_KEY" \
4 -H 'Content-Type: application/json' \
5 --data '{
6 "name": "Fleet registry updates",
7 "nNumbers": ["N172SP"],
8 "endpointUrl": "https://YOUR_PUBLIC_DOMAIN.example/webhooks/aircraft",
9 "eventTypes": ["registration_record_changed", "status_changed"],
10 "frequency": "realtime"
11 }'The 201 response returns a watch ID and a signing secret. Store the signing secret privately when it's issued; you cannot retrieve it again later. Do not put the API key or signing secret in a webpage, repository, tracking event, URL, or screenshot.
Once the public receiver is deployed, the API's authenticated synthetic delivery test can exercise the configured watch before the next observed FAA change. A successful local synthetic test is not evidence that your production callback is reachable; test those separately.
A practical rollout is to start with one known registration, confirm a signed test delivery, verify duplicate handling, and monitor failures before expanding the watched fleet. Keep normal API reads as the fallback when your application needs to inspect a record rather than react to a notification.
Choosing polling versus signed delivery
Use bounded read requests when you're prototyping, refreshing a small fleet once a day, or reviewing change history on demand. The API can answer those questions without an exposed callback endpoint.
Use signed webhooks when an existing product must react to changes for a defined set of registrations, with retry handling and a durable processing queue you already operate. The benefit is not that webhooks make the FAA registry update faster; it's that your product doesn't need to continually rediscover which of its tracked registrations changed.
If the only requirement is a one-time registration lookup, begin with the N-number lookup API. If you need to understand what changed, first test the registration changes feed. If the feed solves a recurring problem and delivery is worth running securely, the working receiver example is the shortest path from a local proof to your own implementation.
