Shipping Webhooks Explained: Events Worth Handling
What shipping webhooks are, which shipment events a small app should handle, how signatures stop spoofed events, and sane retry design.
A shipping webhook is an HTTP POST your shipping platform sends to your server the moment something happens to a shipment — purchased, scanned, delivered, or re-priced by the carrier — so your app reacts to events instead of polling tracking numbers on a timer. For a small app, four moments cover almost everything worth automating: the label was bought, the package started moving, it was delivered, and the carrier adjusted the price after the fact. GoatLabels delivers all of these as signed webhooks on every plan, so you can verify each event actually came from us before your code trusts it.
What is a shipping webhook, and why poll less?
Polling is the default instinct: store a tracking number, then ask "anything new?" every fifteen minutes. It works, and for one package it is even fine. It stops being fine the week you have two hundred open shipments, because now you are making thousands of requests a day to learn that almost nothing changed — and your data is still up to fifteen minutes stale when something finally does.
Webhooks invert the arrangement. You register an HTTPS endpoint once, and the platform calls you, once per event, at the moment the event exists. The practical wins for a small app:
- Freshness without cost. Your "delivered" email goes out when the carrier scan lands, not on the next polling tick.
- No wasted cycles. A shipment that sits in a trailer for two days generates zero traffic instead of 192 identical polls.
- A natural audit log. Persist every received event and you get a timeline per shipment for free — invaluable when a buyer disputes what happened.
Polling still has one honest use: backfill. If your endpoint was down for an hour, a reconciliation job that reads current shipment state from the API catches anything you missed. Webhooks first, polling as the safety net — not the other way around.
Which shipment events matter for a small app?
Resist the urge to handle everything on day one. Most small apps get full value from four event moments:
| Event moment | Payload highlights | What your app should do |
|---|---|---|
| Label purchased | Shipment ID, tracking number, final all-in price, label reference | Attach tracking to the order; record the true label cost; trigger the "your order shipped" flow when you hand the parcel over |
| Tracking movement | Status, latest scan, timestamp | Update order status; surface the current state on your own order page so buyers stop asking |
| Delivered | Final scan, delivery timestamp | Close the order; start the review-request or follow-up clock |
shipment.adjustment.created | Shipment ID, adjustment amount and reason | Apply the cost delta to your stored shipment cost; flag the order if the correction is material |
Everything else — exception scans, return-to-sender turns, customs holds — can be layered in later, and until then your reconciliation job plus the audit log covers the rare cases. If you are building the purchase flow itself, the companion tutorial Build shipping into your app in a weekend shows where webhook registration slots into the build.
How do signed webhooks stop spoofed events?
Your webhook endpoint is a public URL that mutates your database. Unprotected, anyone who guesses it can mark orders delivered, corrupt your costs, or probe your error handling. Signing closes that door.
Every GoatLabels webhook delivery carries a signature computed over the payload with a secret only you and GoatLabels hold. Your handler's first act — before parsing, before touching the database — is to recompute the signature from the raw request body and compare it to the header. The rules that keep this airtight:
- Verify against the raw bytes, not a re-serialized version of the parsed JSON. Serializers reorder keys and change whitespace; the signature will not survive it.
- Reject on mismatch with a 4xx and log it. A burst of failed verifications is a probe worth knowing about.
- Keep the secret out of your code. Environment variable or secret manager, rotated if it ever leaks.
- Do not accept unsigned "test" traffic paths in production. One unauthenticated branch defeats the whole scheme.
Signature verification is cheap — a few lines and a constant-time comparison — and it converts your endpoint from "public URL that edits my database" into "channel only GoatLabels can write to."
How should you handle the adjustment event and its wallet delta?
The adjustment event deserves its own design pass because it is the one that touches money after the fact. Carriers re-weigh and re-measure parcels in their networks; when the audited package does not match what was declared, the price is corrected days after delivery. The mechanics are covered in depth in carrier billing adjustments — the webhook is how your app hears about them.
When shipment.adjustment.created arrives:
- Reconcile, do not just notify. The event references the shipment; apply the delta to your stored cost so margin reports stay truthful. Adjustments also exist as records in the API, so a periodic reconciliation can confirm your event-driven numbers match the ledger.
- Understand where the money comes from. Adjustments debit your prepaid wallet. If an adjustment exceeds the remaining wallet balance, the card on file is charged for the difference — so if your app shows a wallet balance anywhere, treat adjustments as a reason it can move without a purchase.
- Flag patterns, not just instances. One adjustment is noise. The same SKU adjusted five times means your catalog weight is wrong, and the webhook stream is where that pattern first becomes visible.
What does a sane retry and idempotency setup look like?
Deliveries fail: your server restarts, a deploy drops a request, a timeout fires. Webhook systems respond by retrying, which means your handler will eventually see the same event twice. Design for it from the start:
- Acknowledge fast. Return 2xx as soon as the signature checks out and the event is durably queued — do the real work (emails, database updates) after acknowledging, not before. Slow handlers get timed out and retried, manufacturing the very duplicates you are trying to survive.
- Make handling idempotent. Key your processing on the event's identifier, or on (shipment ID, event type, timestamp). Processing the same key twice must produce the same end state — "set status to delivered" is naturally idempotent; "increment shipped-count" is not.
- Let ordering be loose. Retries and carrier timing mean a delivery event can arrive before a movement event. Guard state transitions ("never move a delivered order back to in-transit") instead of assuming sequence.
- Backfill on recovery. After downtime, run the reconciliation job against current shipment state rather than waiting for retries to drain.
None of this is exotic — it is maybe an afternoon of care — and it is the difference between a webhook consumer that quietly works for years and one that double-sends "delivered" emails every time you deploy.
FAQ
Are webhooks available on the free plan? Yes. Signed webhooks and unlimited REST API calls are included on every GoatLabels plan; there is no paid tier gating event delivery.
What happens if my endpoint is down when an event fires? Failed deliveries are retried, which is exactly why your handler must be idempotent. Pair that with a reconciliation job that reads shipment state from the API after an outage, and no event is load-bearing on a single delivery attempt.
How do I test my webhook handler safely? Verify three behaviors locally: a correctly signed payload is accepted, a tampered payload is rejected, and the same event delivered twice leaves your database unchanged the second time. Then buy one real cheap label to your own address and watch the live sequence end to end.
Do I still need to poll tracking at all? Only as a backstop. Webhooks carry the real-time flow; a periodic reconciliation against the API catches anything a retry window missed. Polling as the primary mechanism wastes requests and still delivers stale data.
Why did my wallet balance change without a purchase? Almost always a carrier adjustment. Check your shipment.adjustment.created events and the adjustment records in the API — and note that an adjustment larger than the remaining wallet balance falls through to the card on file.