/openapi.jsonThis document
- 200 OK → OpenApiDocument
API reference
Create orders, read their tracking and status history, and hear about them by webhook. Live keys (`ffk_live_…`) act on live data; test keys (`ffk_test_…`) act only in your sandbox, where simulated drivers work each order so webhooks fire. Rate limits per key: 600 requests per 60 s live, 120 in the sandbox; batches also count their lines (50000 an hour live, 1000 in the sandbox). Shippers: a shipper key (`ffs_live_…` / `ffs_test_…`) reads the /shipper routes only — your shipments across your carriers and your status feeds, read-only — with the same rate limits. Fields are added within v2, never renamed or removed.
Base path /api/ext/v2 · revision 2026-12-15 · OpenAPI 3.1 document
/openapi.jsonThis document
/mescope orders:readThe calling key: mode, scopes, carrier and customer
/postman.jsonA Postman collection of this API, generated from this document
/ordersscope orders:readList orders, newest first
Page with `cursor` until `nextCursor` is null. For an incremental sync, keep the newest `updatedAt` you have seen and ask for `updatedSince` it.
| status | query | |
| createdSince | query | Orders created at or after |
| updatedSince | query | Orders changed at or after (incremental sync) |
| customerId | query | One customer's orders (carrier-level keys) |
| cursor | query | The previous page's nextCursor |
| limit | query | Page size, 1–200 (default 50) |
/ordersscope orders:write· Idempotency-Key requiredCreate an order
The order goes through the carrier's intake checks. **201**: the order exists with its tracking number (status PENDING_REVIEW until the carrier accepts it — `order.approved` follows). `submission.status` is `processing` while the order is placed in the background (address pins, price, the carrier's auto-dispatch — usually a second or two), then `accepted`; GET /submissions/{id} or GET /orders/{trackingNumber} show it placed. **202**: it needs a dispatcher: it waits in the carrier's review queue and `submission.issues` says why; poll GET /submissions/{id} or listen for `order.created`. **200**: this order was submitted before (same `externalRef` and details) — its current state, nothing new is made. Sending an `externalRef` again with different details puts the new version in review. A body that doesn't match the schema is a 422 and nothing is stored.
Body: CreateOrderRequest
/orders/{trackingNumber}scope orders:readOne order
| trackingNumber | path · required |
/orders/{trackingNumber}/trackingscope orders:readWhere an order stands: status, milestones, proof of delivery, the last event
| trackingNumber | path · required |
/orders/{trackingNumber}/historyscope orders:readAn order's status history, oldest first (the webhook event types; undone events are left out)
| trackingNumber | path · required |
/submissions/{id}scope orders:readWhat became of a submitted order: being placed, accepted, in review, or rejected with the dispatcher's note
| id | path · required |
/batchesscope orders:write· Idempotency-Key requiredSend many orders for one customer
JSON `lines` (each a BatchLine), or multipart/form-data with a CSV or XLSX `file` read with the customer's saved order-feed mapping. **202**: stored and queued — poll GET /batches/{id} (the Location header) or listen for `batch.completed`. **200**: these exact bytes were sent before for this customer — that batch, nothing new. Only the envelope is refused (**422**: unreadable, no lines, over the line or byte cap, a missing or unknown customer, no saved mapping for a file). A line that doesn't match BatchLine is kept and goes to the carrier's review queue with its reason; the rest go ahead. A line with an `externalRef` is the same order whether it comes in a batch or through POST /orders. **429**: too many open batches for the key, or its hourly line budget (Retry-After).
Body: CreateBatchRequest
/batches/{id}scope orders:readA batch's status and line counts
| id | path · required |
/batches/{id}/linesscope orders:readA batch's lines, in batch order: outcome, status now, tracking number and reasons
| id | path · required | |
| outcome | query | Only lines with this outcome |
| cursor | query | The previous page's nextCursor |
| limit | query | Page size, 1–200 (default 50) |
/webhooksscope webhooks:manageThe key's webhook endpoints
/webhooksscope webhooks:manage· Idempotency-Key requiredAdd a webhook endpoint
Returns the signing secret once. Each subscribed event is POSTed as a `WebhookPayload` (see `webhooks` below), retried with backoff until your endpoint answers 2xx.
Body: CreateWebhookRequest
/webhooks/{id}scope webhooks:manage· Idempotency-Key requiredStop sending to a webhook endpoint (it is disabled; its delivery log stays with the carrier)
| id | path · required |
/inboxscope webhooks:manage· Idempotency-Key requiredThe sandbox's webhook inbox URL (test keys only)
Made on first use, the same URL after. Point a sandbox webhook at it (POST /webhooks) to watch signed deliveries arrive in GET /inbox before you host an endpoint. A live key is a 403.
/inboxscope orders:readThe sandbox inbox's newest deliveries (test keys only)
Each delivery with its event, delivery id, whether its signature verified with the sending webhook's secret, and the body as received.
/shipper/mescope shipper:readThe calling shipper key: mode, scopes, your org and the carriers it reads now
/shipper/shipmentsscope shipper:readYour shipments across every carrier you are linked to, and from your status feeds, newest first
Every carrier that accepted your link request shares the shipments for the account it chose; a paused or ended link shows nothing. Shipments your status feeds name for a carrier not on the platform are merged in (`source: "external"`, id `ext_…`) — one already matched to a linked carrier's order is listed once, as that order. Switch on `source`; `source=carrier` keeps the list to carriers' orders. Page with `cursor` until `nextCursor` is null — keep the filters the same between pages. Drafts and quotes are never shipments.
| carrierId | query | Only this carrier's shipments (an id from `carrier.id` — a carrier not on the platform's is `ext:<key>`) |
| source | query | Only carriers' orders (`carrier`) or only shipments from your status files (`external`); both when absent |
| status | query | One status, or several comma-separated (e.g. `PICKED_UP,DELIVERED`): an order status narrows carriers' orders, a status-file status (ExternalStatus) your external shipments; a value in both narrows both. Drafts and quotes are never shipments. |
| createdSince | query | Shipments created at or after |
| createdBefore | query | Shipments created before |
| cursor | query | The previous page's nextCursor |
| limit | query | Page size, 1–200 (default 50) |
/shipper/shipments/{id}scope shipper:readOne shipment: status, milestones, arrival window, last scan, exceptions and proof on file — or, for an `ext_…` id, its status-file updates
An `ext_…` id that has since matched a linked carrier's order answers that order (`source: "carrier"`, its own id) — use that id from then on.
| id | path · required |
| line1* | string | null | |
| line2* | string | null | |
| city* | string | null | |
| state* | string | null | |
| postalCode* | string | null | |
| text* | string | null |
A street address: line1 + city + state, or `text`
| line1 | string | |
| line2 | string | |
| city | string | |
| state | string | |
| postalCode | string | |
| text | string | The whole address as one line, when you don't have it in parts |
| id* | string | |
| status* | BatchStatus | |
| reference* | string | null | |
| customerId* | string | |
| source* | json | file | |
| fileName* | string | null | |
| counts* | BatchCounts | |
| notify* | combined | per_order | combined: one batch.completed and no per-order order.created; per_order: every order announced too |
| error* | string | null | Why a failed batch couldn't be read |
| receivedAt* | Timestamp | |
| startedAt* | Timestamp | null | |
| completedAt* | Timestamp | null |
| lines* | integer | Lines in the batch (a file: known once it is read) |
| accepted* | integer | Lines that became orders |
| needsReview* | integer | Lines waiting in the carrier's review queue (each line's reasons are in GET /batches/{id}/lines) |
| duplicate* | integer | Lines sent before with the same reference and details: nothing new was made |
| pending* | integer | Lines not worked yet |
| externalRef | string | Your order number. Sending it again with the same details creates nothing; with new details it goes to review. |
| references | Reference[] | |
| serviceLevel | string | A service level code the customer is offered; absent = the customer's default for the priority |
| priority | UNKNOWN | ROUTINE | SAME_DAY | RUSH | STAT | CRITICAL | |
| pickup | StopInput | |
| delivery* | StopInput | |
| readyTime | string | When the goods are ready |
| deliveryDeadline | string | Deliver by; absent = the service level's clock |
| items | ItemInput[] | |
| packageDescription | string | |
| notes | string | Special instructions |
| cod | CodInput |
| lines* | BatchLineResult[] | |
| nextCursor* | string | null |
What the batch made of the line when it was worked
| index* | integer | 0-based position in the batch (a file: the order its rows were read in) |
| path* | string | Where it was: "lines[37]" or "row 12" |
| externalRef* | string | null | |
| outcome* | BatchLineOutcome | |
| submissionId* | string | null | GET /submissions/{id} follows it after the batch (a dispatcher may approve or reject it later) |
| status* | SubmissionStatus | null | The line's status now |
| trackingNumber* | string | null | |
| issues* | Issue[] | Why it is in review (errors) or what was adjusted (warnings) |
| batch* | Batch |
queued = stored, waiting its turn; processing = lines being made into orders; completed = every line worked; failed = the payload couldn't be read (nothing was made — see `error`)
| event* | string | |
| eventId* | string | |
| label* | string | |
| occurredAt* | Timestamp | |
| batch* | object |
| id* | string | The shipment's id (GET /shipper/shipments/{id}) — the carrier's order id |
| source* | string | A linked carrier's order |
| carrier* | object | The carrier delivering it |
| trackingNumber* | string | |
| status* | OrderStatus | |
| statusLabel* | string | The status in plain words |
| createdAt* | Timestamp | |
| deliveredAt* | Timestamp | null | |
| expectedBy* | Timestamp | null | The delivery deadline while it is open |
| from* | string | null | City, state of the pickup |
| to* | string | null | City, state of the delivery |
| account* | object | Your account at this carrier that the shipment is billed to |
| references* | Reference[] | The shipment's non-sensitive references (PO, BOL, ASN …) |
| id* | string | The shipment's id (GET /shipper/shipments/{id}) — the carrier's order id |
| source* | string | A linked carrier's order |
| carrier* | object | The carrier delivering it |
| trackingNumber* | string | |
| status* | OrderStatus | |
| statusLabel* | string | The status in plain words |
| createdAt* | Timestamp | null | |
| deliveredAt* | Timestamp | null | |
| expectedBy* | Timestamp | null | The delivery deadline while it is open |
| from* | string | null | City, state of the pickup |
| to* | string | null | City, state of the delivery |
| account* | object | Your account at this carrier that the shipment is billed to |
| references* | Reference[] | The shipment's non-sensitive references (PO, BOL, ASN …) |
| cancelled* | boolean | |
| milestones* | object[] | The tracking page's milestones, in order; `at` is null until reached |
| arrivalWindow* | object | null | The carrier's current arrival window, while one is known |
| lastScan* | object | null | The latest custody scan point |
| exceptions* | object[] | Exceptions at a stop, by the carrier's label |
| proof* | object | Proof of delivery on file: a signature, and how many photos |
Collect on delivery: the amount the driver collects at the door
| amountDueCents* | integer | |
| currency* | string | ISO 4217 code of the amount, e.g. USD |
Collect on delivery: the driver collects this amount when delivering
| amountDueCents* | integer | What the driver collects at the door, in cents |
| currency | string | ISO 4217; the carrier's currency (USD) when absent |
| file* | string | A CSV or XLSX file (at most 25 MB), read with the customer's saved order-feed mapping |
| customerId | string | The customer every line is for (a carrier-level key). Ignored for a customer-scoped key. |
| customerCode | string | Or the carrier's code for the customer |
| reference | string | Your own id for this batch (shown back in its status and webhook) |
| customerId | string | The customer every line is for (a carrier-level key). Ignored for a customer-scoped key. |
| customerCode | string | Or the carrier's code for the customer |
| reference | string | Your own id for this batch (shown back in its status and webhook) |
| serviceLevel | string | A service level code for every line that doesn't name one |
| serviceDate | string | The day the lines should ride (YYYY-MM-DD), when it is later than the next cut-off |
| lines* | BatchLine[] | The order lines (at most 5,000 unless your carrier set otherwise) |
| customerId | string | The customer the order is for (a carrier-level key). Ignored for a client-scoped key. |
| customerCode | string | Or the carrier's code for the customer |
| externalRef | string | Your order number. Sending it again with the same details creates nothing; with new details it goes to review. |
| references | Reference[] | |
| serviceLevel | string | A service level code the customer is offered; absent = the customer's default for the priority |
| priority | UNKNOWN | ROUTINE | SAME_DAY | RUSH | STAT | CRITICAL | |
| pickup | StopInput | |
| delivery* | StopInput | |
| readyTime | string | When the goods are ready |
| deliveryDeadline | string | Deliver by; absent = the service level's clock |
| items | ItemInput[] | |
| packageDescription | string | |
| notes | string | Special instructions |
| cod | CodInput |
| name* | string | |
| url* | string | An https URL that answers 2xx |
| events* | order.created | order.approved | order.rejected | order.cancelled | order.dispatched | stop.en_route | stop.arrived | order.picked_up | order.delivered | order.completed | order.escalated | exception.raised | batch.completed[] | |
| customerId | string | null | Only this customer's orders (carrier-level keys); absent = every customer. A client-scoped key always gets its own customer. |
| id* | string | |
| code* | string | null | |
| name* | string |
| error* | object |
| id* | string | `ext_…` — a shipment your status files name (GET /shipper/shipments/{id}) |
| source* | string | A shipment from your own status feeds, for a carrier not on the platform |
| carrier* | object | The carrier as its status files name it |
| trackingNumber* | string | Your reference for it, as the status files give it |
| status* | ExternalStatus | |
| statusLabel* | string | The status in plain words |
| createdAt* | Timestamp | |
| deliveredAt* | Timestamp | null | |
| expectedBy* | Timestamp | null | The latest delivery estimate the files gave, while it isn't delivered |
| from* | null | A status file doesn't name the pickup |
| to* | null | A status file doesn't name the delivery address |
| account* | null | Not billed to an account here |
| references* | object[] | Every reference the status files named |
| lastEventAt* | Timestamp | null | When the latest status happened |
| lastPlace* | string | null | City, state of the latest status |
| id* | string | `ext_…` — a shipment your status files name (GET /shipper/shipments/{id}) |
| source* | string | A shipment from your own status feeds, for a carrier not on the platform |
| carrier* | object | The carrier as its status files name it |
| trackingNumber* | string | Your reference for it, as the status files give it |
| status* | ExternalStatus | |
| statusLabel* | string | The status in plain words |
| createdAt* | Timestamp | |
| deliveredAt* | Timestamp | null | |
| expectedBy* | Timestamp | null | The latest delivery estimate the files gave, while it isn't delivered |
| from* | null | A status file doesn't name the pickup |
| to* | null | A status file doesn't name the delivery address |
| account* | null | Not billed to an account here |
| references* | object[] | Every reference the status files named |
| lastEventAt* | Timestamp | null | When the latest status happened |
| lastPlace* | string | null | City, state of the latest status |
| events* | object[] | The status files' updates, newest first |
| trackingNumber* | string | |
| events* | StatusEvent[] |
| id* | string | |
| url* | string | Point a sandbox webhook at this URL; its deliveries appear in GET /inbox |
| createdAt* | string |
| id* | string | |
| receivedAt* | string | |
| event* | string | null | X-Fleetfully-Event |
| deliveryId* | string | null | X-Fleetfully-Delivery |
| signatureValid* | boolean | The X-Fleetfully-Signature verified with the sending webhook's secret |
| body* | any | The delivery body as received (a WebhookPayload for a webhook delivery) |
| messages* | InboxMessage[] | Newest first, at most 50 |
| inbox* | Inbox |
Why a submission is in review (errors) or what was adjusted (warnings)
| severity* | error | warning | |
| path* | string | |
| code* | string | |
| message* | string |
| sku* | string | null | |
| description* | string | null | |
| quantity* | integer | |
| unit* | EACH | SET | CASE | PALLET | |
| externalLineId* | string | null |
A line: a SKU or a description
| sku | string | |
| description | string | |
| quantity | integer | |
| unit | EACH | SET | CASE | PALLET | |
| externalLineId | string | Your line id |
| key* | object | |
| carrier* | object | |
| customer* | Customer | null | The one customer a client-scoped key reads; null = carrier-level |
| trackingNumber* | string | |
| status* | OrderStatus | |
| customer* | Customer | |
| priority* | UNKNOWN | ROUTINE | SAME_DAY | RUSH | STAT | CRITICAL | |
| serviceLevel* | string | null | |
| externalRef* | string | null | |
| references* | Reference[] | |
| stops* | Stop[] | |
| items* | Item[] | |
| packageDescription* | string | null | |
| cod* | Cod | null | Collect on delivery; null = nothing to collect |
| readyTime* | Timestamp | null | |
| deliveryDeadline* | Timestamp | null | |
| pickedUpAt* | Timestamp | null | |
| deliveredAt* | Timestamp | null | |
| createdAt* | Timestamp | |
| updatedAt* | Timestamp | |
| trackingUrl* | string | null | The public tracking page, where the platform has one |
| orders* | Order[] | |
| nextCursor* | string | null |
| order* | Order |
| kind* | CUSTOMER_REF | INTERNAL_REF | PO | BOL | ASN | DEALER_ORDER | SPECIMEN | REQUISITION | PATIENT_ID | PATIENT_NAME | OTHER | |
| value* | string |
| shipments* | Shipment[] | Newest first, across every carrier |
| nextCursor* | string | null |
| shipment* | ShipmentDetail |
| event* | string | |
| eventId* | string | The same on every retry of this event |
| trigger* | string | What happened: the carrier's event type (order.picked_up, exception.raised …), or for an external shipment `status_file.<status>` (status_file.in_transit …) |
| label* | string | What happened, in plain words |
| occurredAt* | Timestamp | |
| shipment* | ShipmentDetail | The shipment as it is when the POST is sent |
| key* | object | |
| shipper* | object | |
| carriers* | object[] | The carriers whose shipments the key reads now (ACTIVE links) |
One status change — the same types and names the webhooks send
| id* | string | |
| type* | order.created | order.approved | order.rejected | order.cancelled | order.dispatched | stop.en_route | stop.arrived | order.picked_up | order.delivered | order.completed | order.escalated | exception.raised | |
| label* | string | |
| occurredAt* | Timestamp | |
| leg* | PICKUP | DELIVERY | null |
| type* | PICKUP | DELIVERY | |
| sequence* | integer | |
| name* | string | null | |
| address* | Address | |
| window* | object | |
| arrivedAt* | Timestamp | null | |
| departedAt* | Timestamp | null |
| name | string | The business or place |
| address* | AddressInput | |
| contact | object | |
| notes | string | For the driver at this stop |
| window | WindowInput | |
| locationCode | string | Your code for this location (a store or dock number) |
| id* | string | |
| status* | SubmissionStatus | |
| externalRef* | string | null | |
| issues* | Issue[] | |
| trackingNumber* | string | null | |
| resolutionNote* | string | null | The dispatcher's note when it was rejected |
| receivedAt* | Timestamp | |
| updatedAt* | Timestamp |
| submission* | Submission | |
| order* | Order | null |
accepted = an order exists; needs_review = a dispatcher must fix it in Needs you; processing = being created, or created and still being placed (address pins, price, auto-dispatch) — the order and its tracking number are already there; rejected = a dispatcher declined it
ISO 8601 UTC timestamp
| trackingNumber* | string | |
| status* | OrderStatus | |
| pickedUpAt* | Timestamp | null | |
| deliveredAt* | Timestamp | null | |
| deliveryDeadline* | Timestamp | null | |
| proofOfDelivery* | object | |
| lastEvent* | StatusEvent | null | |
| trackingUrl* | string | null |
| id* | string | |
| name* | string | |
| url* | string | |
| events* | string[] | |
| customerId* | string | null | |
| status* | ACTIVE | DISABLED | |
| createdAt* | Timestamp |
| webhook* | Webhook | |
| signingSecret* | string | Verify X-Fleetfully-Signature with it. Shown once. |
| webhook* | Webhook |
| webhooks* | Webhook[] |
| event* | order.created | order.approved | order.rejected | order.cancelled | order.dispatched | stop.en_route | stop.arrived | order.picked_up | order.delivered | order.completed | order.escalated | exception.raised | |
| eventId* | string | |
| label* | string | |
| occurredAt* | Timestamp | |
| order* | object | |
| stop* | object | null | |
| reason* | string | null | For exception.raised and order.escalated: the reason |
When the stop may be served: earliest and latest arrival
| start | string | ISO 8601 timestamp with an offset |
| end | string | ISO 8601 timestamp with an offset |