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 is on v1 for now. An inventory service, InventoryIntegrationService, is the
direction, and its contract is published in the inventory
reference. Its routes do not answer on
https://api.zentail.com yet, so a call to /v2/inventory fails. Until they do, report stock
with the v1 route in step 6. This page will move to the /v2/inventory
routes when they are reachable.
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": "warehouses_bound", "state": "CHECK_STATE_PASS", "message": "1 warehouse(s) bound to this integration", "source": "CHECK_SOURCE_ZENTAIL" } ]}Look for warehouses_bound first. 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 read the warehouse mapping with
GET /v1/warehouses.
Stock updates key on the identifier it returns, and v1 is the only route to it today.
curl -X GET "https://api.zentail.com/v1/warehouses" \ -H "Authorization: <access token>" \ -H "Accept: application/json"{ "results": [ { "warehouseId": 3, // Zentail's own numeric id "warehouseUniqueId": "Warehouse 0001", // your id, as the seller mapped it "name": "Main Warehouse", "canUpdateInventory": true, // false when another service owns this stock }, ],}Store warehouseUniqueId against your own warehouse record. When canUpdateInventory is
false, another service owns that warehouse’s stock and Zentail ignores your updates for it,
so skip it in step 6.
GET /v1/warehouse/{id}
returns one warehouse’s name and address.
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 /v1/inventory.
This is the route to call today. InventoryIntegrationService is not yet reachable on
https://api.zentail.com, as the introduction explains.
One request carries up to 50 entries in products. Name the warehouse with the
warehouseUniqueId from step 1.
curl -X POST "https://api.zentail.com/v1/inventory" \ -H "Authorization: <access token>" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "validAsOf": "2026-03-27T15:53:28-04:00", "products": [ { "SKU": "SKU_00000", "onhand_quantity": 1389, "warehouseUniqueId": "Warehouse 0001" } ] }'Send validAsOf when you can. It states when the counts were true, which lets Zentail order
two updates that arrive out of order. A SKU that does not exist in the seller’s account is
created with the quantity you sent.
Send exactly one quantity field per entry:
| Field | What Zentail does with it |
|---|---|
onhand_quantity |
Sets the physical count. Zentail subtracts what its open orders have reserved and offers the rest for sale |
quantity |
Sets the quantity available for sale to exactly this number, with no reservation subtracted |
delta_quantity |
Adds this number to the quantity Zentail currently shows |
Send onhand_quantity if your platform does not reserve stock for open orders, and let Zentail
do that subtraction. Send quantity only if your platform has already taken reservations out.
To read levels back, use
GET /v1/inventory
for a list or
GET /v1/inventory/{SKU}
for one SKU.
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, and the 3PL reference for the v1 routes this page uses.
- Pagination, Errors, Timestamp formats and Throttle limits.
If anything here is unclear, email [email protected].
API changelog · Built 0c509dd3