3PL integration guide
This is the ordered path for a third-party logistics provider (3PL): a business that stores a Zentail seller’s stock in its own warehouses and ships that seller’s orders. A 3PL integration has two jobs. It ships the orders Zentail routes to it, and it tells Zentail how much stock it holds.
Shipping goes through the Shipping profile’s routes. Every shipping step below uses
FulfillmentIntegrationService, the same /v2/shipping routes the shipping integration
guide documents in full. This page gives the 3PL path
through them and links that guide for the detail, rather than repeating it.
Stock goes through the Inventory profile’s routes. The warehouse lookup in step 1 and
the stock updates in step 6 use InventoryIntegrationService, the same /v2/inventory
routes the inventory integration guide documents in
full.
The same token covers both jobs.
What you’ll build
Section titled “What you’ll build”A poller that, every few minutes:
- reads the work Zentail wants shipped from your warehouses,
- records your own id for each piece of work,
- reports each package as it ships, or declines what you cannot ship,
- pulls back anything Zentail cancels,
- raises an alert when something holds a shipment up,
- and sends your current stock levels back to Zentail.
Before you start
Section titled “Before you start”- Register your application. Fill out the registration form with a unique application name, a contact email, a callback URI for the authentication flow, and a square logo at least 50px by 50px. Zentail sends back the client id and client secret that the authentication flow needs.
- Ask for two scopes.
sales:orders:fulfillment:selfadmits the shipping routes and their v1 counterparts.inventory:selfadmits the inventory and warehouse routes, and limits them to your own warehouses. Registration fixes your scopes, and they cannot be widened later for a seller who has already installed you. See Scopes. - The seller maps their warehouses to you. When a seller installs your application, they pick your integration as the warehouse’s fulfiller and its inventory source, and enter the warehouse identifier your own system uses. Until a warehouse is mapped, nothing is routed to you.
Every call goes to https://api.zentail.com. Send the access token as the whole
Authorization header value, with no Bearer prefix. Each token belongs to one
integration, which is one install of your application. A seller can install you more than
once, so hold one token per integration and run the loop once per token.
No /v2/shipping request carries a warehouse id or a company id. The token resolves to your
integration, and through it to the warehouses mapped to it.
The /v2/shipping routes send every 64-bit integer as a JSON string, so fulfillmentOrderId
and orderNumber arrive as "10012345". They also page and report errors differently from the
v1 routes. See
Pagination and Errors.
1. Check you are wired up
Section titled “1. Check you are wired up”Request
GET /v2/shipping/status.
It returns a list of diagnostic checks, and it answers before any work exists, so it is the
first call to make.
{ "checks": [ { "name": "integration_active", "state": "CHECK_STATE_PASS", "message": "this integration is active in Zentail", "source": "CHECK_SOURCE_ZENTAIL" }, { "name": "warehouses_bound", "state": "CHECK_STATE_PASS", "message": "1 warehouse(s) bound to this integration", "source": "CHECK_SOURCE_ZENTAIL" } ]}Look for integration_active first. It fails when the integration has been switched off in
Zentail. Every acknowledge call is then refused, and so are GET /v2/inventory,
POST /v2/inventory and GET /v2/inventory/warehouses. You can still ship orders, reject
them, confirm the cancellations Zentail asks for, and raise alerts.
Because you can’t acknowledge, a cancellation of an order you haven’t acknowledged never
reaches you. Before you ship such an order, check that it is still in need_to_ship.
To get the integration switched back on, ask the seller to contact Zentail support at [email protected]. See Errors.
Then look for warehouses_bound. It fails until a warehouse names your integration as its
fulfiller. Match on a check’s name, never on its message. The
shipping guide lists every
field a check carries.
Then list the warehouses your integration can stock with
GET /v2/inventory/warehouses.
Stock updates in step 6 key on the warehouseUniqueId it returns.
curl -X GET "https://api.zentail.com/v2/inventory/warehouses" \ -H "Authorization: <access token>" \ -H "Accept: application/json"{ "warehouses": [ { "warehouseUniqueId": "Warehouse 0001", "name": "Main Warehouse" } ]}warehouseUniqueId is your own identifier for the warehouse, as the seller mapped it. Store
it against your own warehouse record, and re-read the list at startup rather than
hard-coding it. name is a label for logs, so never match on it.
2. Poll the work you owe
Section titled “2. Poll the work you owe”Request
GET /v2/shipping/need_to_ship.
It returns the fulfillment orders you owe a shipment on.
A fulfillment order is the part of one customer order routed to one of your warehouses.
One order split across two of your warehouses gives you two fulfillment orders. Every write
below names its work by fulfillmentOrderId. orderNumber is carried for display only, and
no write accepts it.
Pass cursor and pageSize, and keep requesting until nextCursor comes back empty. There
is no time filter, on purpose: the queue is drained by confirming work, never by moving a
clock forward, so you cannot skip past work you failed to finish.
Three rules keep a poller correct:
- Read
linesfresh on every poll, and never cache it. A partial re-route lowers a line’squantityrather than removing the fulfillment order. - A line’s
quantityis what is routed to your warehouse and still owed, not what the buyer ordered. - Do not drive behaviour from
status. The queues already say what Zentail wants done.
The shipping guide’s step 2 shows a full fulfillment order and explains each field, including the money fields and the ones that are always empty today.
3. Acknowledge what you have created
Section titled “3. Acknowledge what you have created”When you create the work in your own system, record your id for it with
POST /v2/shipping/fulfillment_orders/acknowledge:
{ "acknowledgements": [ { "fulfillmentOrderId": "88120045", "externalOrderId": "WMS-ORD-5531" } ]}Acknowledging does not remove work from need_to_ship, and it is not required before
shipping. Do it anyway. It is what puts the work into the cancel queue in step 5, and it lets
you tell, after a crash, which work you had already created.
Send at most 50 per call. Read each entry in results rather than the HTTP status, because one
entry failing does not fail the rest. alreadyAcknowledged: true means a retry, and counts as
success.
4. Ship it, or reject it
Section titled “4. Ship it, or reject it”Report each package as it ships with
POST /v2/shipping/shipments.
It removes the shipped quantities from need_to_ship.
{ "shipments": [ { "fulfillmentOrderId": "88120045", "externalShipmentId": "PKG-000918", "carrier": "UPS", "trackingNumber": "1Z999AA10123456784", "serviceLevel": "Ground", "shippingCost": { "amount": "8.41", "currencyCode": "USD" } } ]}fulfillmentOrderId, externalShipmentId, trackingNumber and serviceLevel are refused
when empty, so send all four. externalShipmentId is your package id and the idempotency key:
replaying a shipment changes nothing, and alreadyRecorded: true says so. Leave out lines to
ship everything the fulfillment order still owes.
Send tracking once, and get it right. Once Zentail passes it to the sales channel, most channels do not accept a correction.
If you cannot ship the work at all, decline it with
POST /v2/shipping/fulfillment_orders/reject
and a reason, such as REJECTION_REASON_OUT_OF_STOCK. Zentail then reroutes it or shows it
to the seller. Reject the whole fulfillment order: naming lines fails the entry.
The shipping guide’s step 4 covers the optional fields, the cost fields and every rejection reason.
5. Handle cancellations and alerts
Section titled “5. Handle cancellations and alerts”Request
GET /v2/shipping/need_to_cancel
on the same loop. It returns acknowledged fulfillment orders that Zentail now wants pulled
back, and it pages the same way as need_to_ship. Once you have stopped the work, confirm it
with
POST /v2/shipping/fulfillment_orders/confirm_cancellations:
{ "confirmations": [{ "fulfillmentOrderId": "88120045" }] }If the package has already gone, still confirm, with alreadyShipped: true, and report the
package through step 4. Zentail needs both to keep the customer order truthful.
When something is holding a shipment up and you still mean to ship it, raise an alert with
POST /v2/shipping/fulfillment_orders/alerts.
The seller sees it on the order. Raising is idempotent, so re-raise a condition on every poll
without keeping your own record. Clear it with
POST /v2/shipping/fulfillment_orders/alerts/resolve.
An alert is not a substitute for rejecting: reject work you will not ship.
The shipping guide’s step 5 and step 6 cover cancellation reasons, partial cancellations and the alert types.
6. Send stock levels
Section titled “6. Send stock levels”Tell Zentail how much of each SKU you hold with
POST /v2/inventory.
Name each warehouse by the warehouseUniqueId from step 1.
curl -X POST "https://api.zentail.com/v2/inventory" \ -H "Authorization: <access token>" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "observedTs": "2026-09-19T14:24:24Z", "updates": [ { "sku": "SKU_00000", "warehouseUniqueId": "Warehouse 0001", "quantity": 1389 } ] }'observedTsis required. It is when you counted, not when you sent. Zentail discards an update older than what it already holds, so a slow retry can’t roll a count backwards.- One request carries at most 50 updates. A larger one fails whole.
- A
200does not mean every update landed. Each update gets its own result. Match results onskuandwarehouseUniqueId, and branch onfailureReason. The inventory guide explains each reason, and which ones to retry.
See also
Section titled “See also”- The shipping integration guide, which covers every shipping step in full, including reconciling after a crash.
- The shipping reference for the shipping routes, the inventory reference for the stock routes, and the 3PL reference for the v1 routes in the asides.
- Pagination, Errors, Timestamp formats and Throttle limits.
If anything here is unclear, email [email protected].
API changelog · Built f1d143e6