Event model

Updated May 26, 20266 min read

Carriyo emits an event for every meaningful state change: a shipment booked, a label ready, a parcel delivered, a return requested. Your systems subscribe to these events as webhooks and react in real time. This page covers the model: what an event is, how it gets to you, what guarantees come with it, what doesn't. For the mechanics of registering and securing webhook subscriptions, see Webhooks.

What emits an event

State changes on Carriyo's customer-facing entities emit events:

  • Order events. Created, updated, cancelled, fulfilled, unfulfilled.
  • Fulfillment order events. Created, allocated, shipped, cancelled.
  • Shipment events. Created, confirmed, label ready, every status transition, delivered, cancelled.
  • Return events. Requested, approved, rejected, received, completed.
  • Inventory events. Coming soon.

Each event is dispatched to the webhook subscriptions you've registered. A subscription can be scoped to specific merchants, specific triggers, and specific payload conditions. See Webhook configurations.

Warehouse-floor entities (Pick, Pack, Collection) progress through their own statuses inside the Fulfillment App. Your systems subscribe to the parent Shipment, FulfillmentOrder, and Order events that warehouse progression drives, not to the floor entities directly.

Status events: the ledger behind every webhook

Every accepted state change is recorded as an immutable status event before any webhook is dispatched. The status-event history is the canonical record of what happened, and the customer-facing tracking timeline renders from it.

This distinction matters for reconciliation. Webhooks are the notification channel; status events are the ledger. If you ever need to reconstruct what happened on a shipment, the ledger is authoritative. It's ordered by status timestamp, never rewritten on retries, and exposed through the tracking endpoints. Webhooks, by contrast, are at-least-once notifications fired from those events. The same underlying change may produce multiple deliveries (retries) carrying the same event id.

Delivery characteristics

Event delivery is asynchronous and decoupled from the request that caused the state change. The properties integrators should rely on:

  • Back-pressure absorption. A burst of inbound carrier events is buffered, not dropped. Your endpoint receives a steady flow of events, not a spike.
  • Failure isolation. A slow or failing subscriber doesn't block delivery to other subscribers. Each subscription is delivered independently.
  • Automatic retry. Failed deliveries are retried on the schedule below. No manual orchestration on your side.
  • Replay. Any delivery, successful or failed, can be re-fired through the Retrigger API.

The practical implication: don't expect a webhook in the same tick that your API call returns. Delivery is typically sub-second from the underlying state change, but the model is eventual and your handlers should be designed for it.

Event payload structure

Carriyo does not wrap webhook bodies in an envelope. The body of every POST is the full current entity: the entire Shipment, Order, FulfillmentOrder, Pick, Pack, Collection, or ReturnRequest JSON, identical to what you'd receive from the equivalent GET on the API. The shape is the entity, not a wrapper.

Envelope-style metadata travels in HTTP headers, set on every delivery:

HeaderPurpose
event-idStable hash unique to this state change. Same value on every retry of the same event.
triggerCanonical status code or event name that fired the webhook (e.g. delivered, label_ready, status_update).
webhook-idThe id of your subscription configuration.
last-retryPresent only when the next attempt will be the final one.
AuthorizationCarries a bearer token if you've configured OAuth or a static auth header on the subscription.
Content-TypeAlways application/json; charset=utf-8.

Treat headers as authoritative for routing and dedup, and the body as authoritative for state.

Event ID and idempotency contract

The event-id header is deterministic: it's a hash of the subscription id and the serialized payload, so every retry of the same state change carries the same id. This is what makes a true idempotent handler possible. Store each event-id you've successfully processed, and short-circuit duplicates.

Carriyo guarantees at-least-once, not exactly-once. A re-trigger from the Retry failed webhook events API will produce the same event-id for the same underlying state, so re-triggers also dedupe naturally if your handler keys on event id.

Implementation note

An idempotent handler is required, not optional. Network flakiness, your endpoint timing out, an automatic retry: any of these produces duplicate deliveries. A handler that doesn't dedupe will eventually double-count or double-act.

Retry schedule

Failed deliveries (any non-2xx response, connection error, or timeout) are retried automatically on a Fibonacci schedule:

TierAttemptsCadenceHorizon
Default31 min, 3 min, 5 min~9 minutes
Extended5 additional1 hr, 3 hr, 5 hr, 8 hr, 13 hr~27 hours

Extended retries are an add-on. Contact your Carriyo account team if you need the longer horizon. Retries are dispatched through the same outbound channel as fresh deliveries, so your endpoint sees them mixed in. The event-id header is the reliable way to distinguish a retry from a new event.

Every attempt, successful or failed, is recorded. The Dashboard shows this in the integration health view, and the retry API lets you manually re-fire any failed delivery.

Ordering and out-of-order delivery

Carriyo does not guarantee delivery order on webhooks. A failed-and-retried picked_up event may arrive after the subsequent delivered event for the same shipment. Each delivery is independently retried on the schedule above.

Your handlers must be state-driven, not sequence-driven:

  • Read the timestamps inside the payload (postShippingInfo.statusDate or update_date) to know when the event actually occurred.
  • Ignore any event whose status precedes your local state for that entity.
  • Use the Shipment status flow to validate which transitions are legal. An out-of-order event for an invalid transition is the signal it should be ignored.

The same principle applies across entities: a Pack-complete event arriving before its parent FO is allocated is a re-ordering artifact, not a bug.

Status propagation

A status change on one object often propagates up the entity graph. Each transition fires its own webhook so you can react at any level of granularity:

  • A Shipment marked delivered may transition its fulfillment order to completed.
  • All FOs being completed may transition the parent Order to fulfilled.
  • A Return marked received may transition its parent Order to partially returned.

Propagation is automatic but predictable. Every transition is documented in the per-entity state-machine reference. For the finest-grained events, subscribe at the entity level (Shipment, FO, Return) rather than only at Order level.

See the canonical status catalogues:

What does NOT emit an event

Not every database write produces a webhook. Internal mutations are filtered out before the dispatcher sees them:

  • Label-URL refreshes (the URL changes, but the Shipment didn't move state).
  • Schedule recalculations and SLA re-projections.
  • Statistics counters and analytics roll-ups.
  • Idempotent re-applications of an already-current status (no real state change).

A webhook fires only when the configured trigger field actually moves. If you expected an event and didn't receive one, the underlying field likely didn't change. It may also have matched an exclusion in your subscription's filter conditions.

Delivery latency

Webhook delivery is typically sub-second from the moment the underlying state changes. If you observe consistent multi-second delays, the cause is almost always endpoint-side: long-running handlers blocking the connection, upstream NAT or firewall queue depth, or DNS resolution latency on your side.