Webhooks carry events in two directions.
  • Outbound: the implementer notifies you. This covers webhook endpoints, the event object, signatures, retries and redelivery.
  • Inbound: events flow into the implementer, either as carrier-native pushes received per connection, or as canonical events a client injects (its own last-mile fleet, a carrier with no API, a 3PL feed).
The diagram shows both directions: inbound sources feed trackers, and every resulting change leaves as an outbound event.

1. The event object

  • data.object is the full resource rendered at the endpoint’s api_version. It is never a diff.
  • data.previous_attributes is present on *.updated and *.status_changed events and lists only the changed fields.
  • request.id links the event to the API call that caused it. It is null for carrier-originated events.
  • Events are retained and listable for 30 days.

2. Event catalog

enabled_events accepts * (everything) and resource wildcards (tracker.*).

3. The webhook_endpoint object

  • secret is returned only on create and on rotate.
  • status is enabled, disabled (by the user), or auto_disabled (after 3 consecutive days of failures).
  • filters narrow delivery beyond event type. Every listed filter must match. This keeps multi-tenant platforms from receiving every merchant’s events.

Endpoints

  • url MUST be https. Implementers MUST refuse private, loopback and link-local addresses, which protects against SSRF.
  • rotate_secret keeps the old secret valid for expire_previous_in seconds (0–7 days). During overlap, deliveries are signed with both.
  • test sends a synthetic event built from a realistic fixture of the requested type. The event has livemode: false and is not stored in /v1/events.

4. Delivery and signatures

Each delivery is an HTTPS POST with the event JSON as the body, and these headers:
Signature scheme (identical to Stripe’s, so existing verification libraries work):
  1. signed_payload = "{t}.{raw_body}"
  2. v1 = hex(HMAC_SHA256(secret, signed_payload))
  3. The receiver checks that any v1 matches using a constant-time comparison, and that |now − t| ≤ 300s.
The flowchart shows the checks a receiver runs on each delivery. Implementers SHOULD additionally offer asymmetric signatures (v1a= with Ed25519, public key at /.well-known/shipping-configuration → webhook_signing_keys), so receivers can verify without holding a shared secret. This matters for agents that subscribe dynamically.

Retries and ordering

  • A 2xx response within 10 seconds is success. Anything else is a failure, and the body is ignored.
  • Retries use exponential backoff with jitter over 3 days: 1m, 5m, 30m, 2h, 6h, then every 12h.
  • No ordering guarantee. Use data.object.updated or tracker status_updated_at to discard stale events.
  • At-least-once delivery. Deduplicate on Shipping-Event-Id, which is the same on every attempt. Each attempt is its own webhook_delivery with a new whd_ ID (Shipping-Delivery-Id) and an incrementing attempt.
The sequence shows one event delivered after a failed first attempt.

The webhook_delivery object

5. Events API

Polling /v1/events with starting_after is a supported alternative to webhooks for clients that cannot receive inbound HTTP, such as agents behind NAT.

6. Inbound: carrier push receivers

Many carriers push tracking (and sometimes invoice adjustments) to a URL. Each connection that supports push gets a dedicated receiver:
  • The URL is exposed as connection.inbound.url. The implementer registers it with the carrier automatically where the carrier has a subscription API, and otherwise the shipper pastes it into the carrier portal.
  • The body is carrier-native. The implementer verifies the carrier’s own signature scheme, which is configured in the carrier’s inbound descriptor (chapter 09), maps the payload to canonical events, and responds the way the carrier expects.
  • Each receipt creates an inbound_event with source: "carrier_push". (inbound_event.source is carrier_push for this receiver and api for §7. It describes how the event entered the API. The resulting tracking events carry tracking_event.source values carrier_push and injected respectively.) Payloads that fail signature verification are rejected with 401, logged, and not stored.
The sequence shows one carrier push from receipt to the client’s webhook endpoint.

7. Inbound: event injection (canonical)

For sources that have no carrier integration (a merchant’s own delivery fleet, a regional courier using spreadsheets, a 3PL’s scan feed, a carrier implementing this spec that pushes canonical events), clients inject events directly:
The response is an inbound_event:
Injected events are tagged source: "injected" on the tracker and fire the usual tracker.* events. Injection requires the trackers:write scope, and injection into a tracker owned by a carrier connection additionally requires inbound:write, so that a leaked read key cannot fake deliveries. The sequence shows the injection example above, a merchant fleet recording a delivery.

8. Test helpers

This is only available in test mode. It moves a test tracker through a scripted path and emits real webhook events. Scripts: standard (pre_transit → in_transit ×3 → out_for_delivery → delivered), failed_attempt (… → exception/delivery_attempted → out_for_delivery → delivered), customs_hold, return_to_sender, lost. Also available: POST /v1/test_helpers/shipments/{id}/charge_adjustment, which simulates a carrier reweigh.