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 two authentication methods for webhook delivery:
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.
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} |
| 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, tour?: {id: String, properties: Object}} |
| 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} |
Notes on tour-action-created
tour-action-created- Fires for every action on a tour — status advances, reversals (
reverse), resets (reset), 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.
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.
