Orbit's webhooks facilitate real-time data provision, allowing your application to respond to events as they transpire, without the need for constant polling for changes. This results in a more efficient and often more effective system, reducing server strain.
Consider these webhook events as a reverse API call. Rather than your application requesting data from our API, Orbit pushes the data directly to your application as the specified event takes place.
Orbit's webhooks can be set up to notify about a variety of events. These could include the creation of a new tour, the completion of a tour, or a new shipper registration. By subscribing to these events, your application can automate tasks, align data, or even interact with other systems, thereby streamlining your processes.
You can set up webhooks in Orbit through MissionControl under Settings -> Webhooks.
Authorization
Webhook authorization ensures that incoming requests to your endpoints are genuinely from Orbit, protecting your application from unauthorized or malicious requests. This security layer prevents data tampering and maintains the integrity of your webhook integrations.
Orbit supports three authentication methods for webhook delivery. Whichever you choose, the secret is stored write-only: once saved it is never shown or returned again, and editing a webhook without retyping it keeps the stored value. To change a secret, enter the new one.
Basic Authentication
Basic authentication uses a username and password combination that you configure when setting up your webhook. Orbit includes these credentials in the standard HTTP Authorization header using Base64 encoding. This method provides straightforward security and is widely supported across platforms and frameworks.
Bearer Token Authentication
Bearer token authentication sends a token you configure in the standard HTTP Authorization header:
| Header | Value |
|---|---|
| Authorization | Bearer <your-token> |
The token travels with every delivery, so use it only over HTTPS. It suits endpoints that already accept an API token, and it is the simplest option to verify: compare the received token against the one you configured. For a secret that never crosses the network at all, use HMAC instead.
HMAC Authentication
HMAC (Hash-based Message Authentication Code) provides cryptographically secure authentication using a shared secret. This method offers superior security as the secret never travels over the network.
HMAC uses the HMAC-SHA256 algorithm with your configured secret key. The signed payload always includes the epoch timestamp in seconds and is dot-separated with the stringified JSON body (timestamp.raw_json_body). The signature is encoded in lowercase hexadecimal format.
Headers set by HMAC Authentication:
| Header | Value |
|---|---|
| X-Signature-Algorithm | hmac-sha256 |
| X-Signature-Timestamp | Unix epoch seconds |
| X-Signature | The computed HMAC signature |
To verify authenticity, recreate the signature using your secret and compare it with the received signature. Most applications implement a replay window (typically 5 minutes) to prevent replay attacks by rejecting requests with timestamps outside the acceptable range. HMAC authentication is recommended for production environments requiring enhanced security.
Delivery & Timeouts
Each event is delivered as an HTTP POST with a JSON body and a unique X-Delivery-Id header. The delivery ID stays constant across retries of the same event, so you can use it to deduplicate events on your side.
Orbit waits up to 20 seconds for your endpoint to respond. If no response is received within this timeframe, the delivery is considered failed and is retried once after a short delay. Ensure your endpoint processes requests efficiently or acknowledges receipt quickly and handles longer processing asynchronously.
Filtering Deliveries
Each event subscription can optionally carry a filter — a set of conditions evaluated against the event's payload before delivery. When a filter is set, Orbit only sends the webhook if the payload matches; non-matching events are skipped silently without an HTTP request. This lets you subscribe to a busy event type while receiving only the deliveries you care about, e.g. tour-completed only for tours of a specific region, or shipment-updated only when the shipment has reached a specific status.
Filters are configured per event in MissionControl under Settings -> Webhooks: enable an event, then choose Add Filter on its row. The available fields are derived from that event's payload (the same shape documented in the table below, using dotted paths such as tour.status), so a filter can only reference data the delivery actually contains.
Your own custom properties — the ones created in MissionControl under Settings -> Properties — are filterable too. A property appears as the field <entity>.properties.<propertyId> on events that deliver the whole entity it is defined on, so a property on Tour can be filtered on tour-completed while a property on Order can be filtered on order-updated (id-only events such as tour-deleted offer no property fields). Text, number, checkbox and select properties are filterable today; multiselect, tags and document properties are listed in the field picker but cannot be selected yet. Select conditions compare against the option's id, and the picker inserts it for you when you choose the option by name. On order-created, fields of the optional tour payload entity offer no "is set" / "is not set" conditions — an order without a tour has no tour entity to check them against.
Two events deliver a list rather than a single entity: shipments-created carries an array of shipments and transport-drafts-created an array of transport drafts, so neither offers per-shipment or per-draft filter fields yet — filter shipment-updated instead when you need shipment-level conditions.
A few semantics worth knowing:
- Conditions within a group combine with AND by default (every condition must match); a group can be switched to OR (any condition may match), and groups can be nested.
- Text conditions (
contains,begins with,ends with,equals) match literally and case-sensitively. Each has a negated form (does not contain,does not begin with,does not end with,does not equal). - A negated text condition also matches when the field carries no value. A custom property that was never set, or an optional payload field the event omits, is not equal to anything and contains nothing, so
does not equal Xdelivers those events as well. - Date conditions compare against calendar days anchored at Europe/Berlin.
- A skipped delivery is not a failure: it does not consume retries and never triggers failure alerting. Your endpoint simply doesn't hear about the event.
- A subscription without a filter behaves exactly as before — it fires for every occurrence of the event.
Types and Payloads
In the subsequent table, we provide a description of each webhook event type and the corresponding payload format.
| Event Type | Payload |
|---|---|
| tour-created | {tour: Tour} |
| tour-assigned | {tour: Tour} |
| tour-unassigned-carrier | {tour: Tour} |
| tour-started | {tour: Tour} |
| tour-arrived-at-stop | {tour: Tour} |
| tour-departed-from-stop | {tour: Tour} |
| tour-proof-attached | {tour: Tour} |
| tour-waiting-for-review | {tour: Tour} — fired by the last advance, by a carrier-reported delivery, or by manual-complete |
| tour-completed | {tour: Tour} |
| tour-cancelled | {tour: Tour} |
| tour-deleted | {tourId: String} |
| tour-edited | {tour: Tour} |
| tour-rebuilt | {tour: Tour, action: TourAction} |
| tour-action-created | {tour: Tour, action: TourAction, oldStatus?: String, newStatus?: String} |
| shipments-created | {shipments: Shipment[]} |
| shipment-deleted | {shipmentId: String} |
| shipment-updated | {shipment: Shipment} |
| order-created | {order: Order, tour?: Tour, documentLinks?: DocumentLink[]} |
| order-updated | {order: Order} |
| operator-user-invite-created | {operatorUserInvite: OperatorUserInvite} |
| operator-user-created | {operatorUser: OperatorUser} |
| shipper-created | {shipper: Shipper} |
| shipper-updated | {shipper: Shipper, action: ShipperAction} |
| shipper-team-created | {shipperTeam: ShipperTeam} |
| shipper-team-updated | {shipperTeam: ShipperTeam, action: ShipperTeamAction} |
| shipper-user-created | {shipperUser: ShipperUser} |
| shipper-user-updated | {shipperUser: ShipperUser, action: ShipperUserAction} |
| shipper-address-created | {shipperAddress: ShipperAddress} |
| mailbox-created | {mailbox: Mailbox} |
| mailbox-mail-received | {mailboxId: String} |
| mailbox-status-changed | {mailboxId: String, newStatus: "active" | "inactive", oldStatus: "active" | "inactive"} |
| decode-job-result | {transportDraft: TransportDraft} |
| callout-action-created | {callouts: CalloutWithStats[], action: CalloutAction} |
| sequence-invocation-created | {sequenceInvocation: SequenceInvocation} |
| sequence-invocation-action-created | {sequenceInvocationAction: SequenceInvocationAction} |
| transport-drafts-created | {transportDrafts: TransportDraft[]} |
| transport-drafts-deleted | {transportDraftIds: String[]} |
| transport-draft-action-created | {transportDraft: TransportDraft, action: TransportDraftAction} |
| carrier-action-created | {carrier: Carrier, action: CarrierAction} |
| carrier-team-created | {carrierTeam: CarrierTeam} |
| carrier-team-action-created | {carrierTeam: CarrierTeam, action: CarrierTeamAction} |
| carrier-user-created | {carrierUser: CarrierUser} |
| carrier-user-action-created | {carrierUser: CarrierUser, action: CarrierUserAction} |
| document-link-action-created | {documentLink: DocumentLink, action: DocumentLinkAction} |
| leg-created | {leg: Leg} |
| leg-action-created | {leg: Leg, action: LegAction} |
| leg-deleted | {leg: Leg} |
| transport-draft-recurring-activated | {transportDraft: TransportDraft} |
| transport-draft-recurring-paused | {transportDraft: TransportDraft} |
| transport-draft-recurring-resumed | {transportDraft: TransportDraft} |
| transport-draft-recurring-ended | {transportDraft: TransportDraft} |
| comment-created | {comment: Comment} |
| comment-edited | {comment: Comment} |
| comment-deleted | {comment: Comment} |
| comment-reaction-added | {comment: Comment, reaction: CommentReaction} |
| comment-reaction-removed | {comment: Comment, reaction: CommentReaction} |
| task-created | {task: Task, referencedEntity: {orn: String, type: String, id: String}} |
| task-completed | {task: Task, referencedEntity: {orn: String, type: String, id: String}} |
| task-skipped | {task: Task, referencedEntity: {orn: String, type: String, id: String}} |
| task-cancelled | {task: Task, referencedEntity: {orn: String, type: String, id: String}} |
Notes on tour-action-created
tour-action-created- Fires for every action on a tour — status advances, reversals (
reverse), resets (reset), manual completions (manual-complete), assignments, edits, rebuilds, proof attachments, and more — making it the one subscription that observes the tour's complete activity stream, including backwards status transitions that no lifecycle event covers. action.payload.typeidentifies the action. Combine this event with a delivery filter (see "Filtering Deliveries" above) onaction.payload.typeto receive only the actions you care about, e.g. onlyreverseandreset.oldStatus/newStatuscarry the tour's status before and after the action. They are only present when the originating commit relayed its status snapshot onto this action (the first action of a commit); when present and equal, the action did not change the tour's top-level status (the change may be intour.substatus).- Prefer this event over the individual
tour-*lifecycle events for new integrations that mirror tour state — the lifecycle events remain supported, but new action types are surfaced here only. - Carrier-integration internals are redacted from the delivered
action: raw carrier-provider payloads (e.g. on system-generated carrier status updates) are stripped, and any internal carrier-booking record is reduced to its bounded identifiers (provider,carrierId,carrierBookingId,trackingRef) with integration internals removed — a policy analogous to the bounded carrier-booking view exposed on thetourobject itself. System-stamped internal routing fields (such as the pre-assign carrier reference onassignactions) are likewise omitted, matching the action shape of the REST API.
Notes on callout-action-created
callout-action-createdcalloutscontains only the callouts the action actually modified (e.g. a decline carries just the declining carrier's callout; creating callouts carries the newly activated ones) — it is not the cluster's full callout list.- Each callout is enriched with its cluster's bidding stats (
currentBid,bidCount,isWinning,suggestedBid). For secret auctions onlysuggestedBidis included — bid state is never revealed. action.payloadmirrors the request that drove the transition; internal dispatch context (such as system cancel reasons) is stripped.tourcarries the id and custom properties of the tour the callouts are for, so you can filter this event ontour.properties.<propertyId>— for example to receive only the callout activity for one shipper. Filtering on the callouts themselves is not possible:calloutsis a list, and filter fields cannot address list entries.touris deliberately onlyidandproperties, not the whole tour: everything else a tour carries may belong to other customers loaded on the same trip. Read the tour fromGET /tours/{id}when you need more than the properties.touris absent when the action was not raised against a tour, which happens on system cancellations. A condition on a tour field does not match such a delivery, so a filter narrows rather than leaks.action.createdAtandaction.createdAtMillisecondsare both epoch milliseconds — an explicit exception to the seconds rule in Units & formatting. The two fields carry the same value, and it matches the timestamp stored on the action record, so an event value can be compared directly against one read back from the API.- Compatibility note: before this change
action.createdAtwas delivered in epoch seconds while the stored record already held milliseconds, so the two disagreed by a factor of 1000. Deliveries sent before the change carry the old seconds value. If you persisted or comparedaction.createdAtfrom earlier deliveries, treat any value below1000000000000as seconds and multiply by 1000.action.createdAtMillisecondswas always milliseconds and is unaffected.
Notes on leg events
- There are no per-status leg events: every lifecycle transition — pickup and dropoff progress, delivery, failure, cancellation, tour assignment — is delivered as
leg-action-created. The transition verb is identified byaction.payload.type, and the payload always carries the full leg as it looks after the transition. - Unlike
tour-deletedandshipment-deleted, which deliver only the deleted entity's id,leg-deletedcarries the full removed leg — a deleted leg can no longer be fetched from the API, so its final state is included in the delivery.
Notes on task events
- The four
task-*events cover the complete task lifecycle:task-createdwhen a task comes to exist, and exactly one oftask-completed,task-skipped, ortask-cancelledwhen it closes. Reassignments produce no event. - The
taskobject is the task as it looks after the transition. Ontask-completedit carries the complete validated form output (task.outcome.formOutput, keyed by form element id) along with the task's kind, assignee, origin, and timestamps. referencedEntityis a reference, not the object. Task payloads carry\{orn, type, id}parsed from the task's ORN and never embed the referenced Tour, Shipment, or other entity. Fetch the current state of the referenced entity through the API when you need it — an embedded copy would already be stale by the time your endpoint processes the delivery.
