Inventory retrieval
Your channel oversells when its inventory levels fall behind Zentail’s. This step keeps them current, so run it as often as you can.
What you’ll build
Section titled “What you’ll build”A poller that asks Zentail for the variants whose inventory changed since it last asked, and a check that catches any update the poller missed.
Before you start
Section titled “Before you start”- You hold an access token for the seller. See Authentication.
1. Poll for inventory changes
Section titled “1. Poll for inventory changes”Request
GET /v2/storefront/listing/variant/inventory/updated
with the time of your last successful poll:
GET /v2/storefront/listing/variant/inventory/updated?since=2026-09-22T14:00:00.000Z| Parameter | Type | Description |
|---|---|---|
since |
string ($date-time) |
Only variants whose inventory changed at or after this time. |
cursor |
string |
The nextPageCursor from the previous page. Leave it out for the first page. |
These are the only two parameters. The route returns every variant that has inventory
enabled and has had an inventory change since since.
Each variant carries its full data, but for inventory read only the inventory object:
{ "variants": [ { "sku": "TSHIRT-RED-M", "inventory": { "enabled": true, "totalQuantity": "100", "merchantFulfillableQuantity": "100", "merchantFulfillableWarehouseQuantities": [ { "warehouseId": "main", "quantity": "100" } ], "storefrontFulfillableQuantity": "0", "updatedAt": "2026-09-22T14:03:11.000Z" } } ], "nextPageCursor": ""}That is the whole inventory object — every field the schema defines for it.
- Every quantity is a JSON string, not a number. That covers the three on the
inventoryobject and thequantityinside each entry ofmerchantFulfillableWarehouseQuantities. All four are 64-bit integers, which JSON numbers cannot carry safely. Parse them as integers. totalQuantityis usually all you need. It is the quantity available for sale now, after pending orders across all of the seller’s channels. It should equalmerchantFulfillableQuantityplusstorefrontFulfillableQuantity, and the schema states it that way rather than as a guarantee. TreattotalQuantityas authoritative, and do not reject a payload whose parts disagree with it.updatedAtis when the inventory level last changed.
Keep requesting with cursor set to nextPageCursor until it comes back empty.
A high-frequency poller can call
GET /v2/storefront/listing/variant/inventory/updated/skus
instead. It takes the same parameters and returns skus and nextPageCursor rather than
whole variants. Page through it the same way, and fetch each SKU you need with
GET /v2/storefront/listing/variant/{sku}.
Recommended implementation strategy
Section titled “Recommended implementation strategy”Keep one timestamp per seller: the time of the most recent successful poll. For each seller, run a process that requests changes since that time and processes every page. Then move the timestamp to the time that process began.
2. Catch what the poll missed
Section titled “2. Catch what the poll missed”The poll reports a variant once, when its inventory timestamp advances. An update your
process drops is never sent again, so that SKU stays stale until its next real change.
GET /v2/storefront/listing/variant/inventory/drift/skus
catches those.
It returns the SKUs where the quantity you last told Zentail you sent (step 3) disagrees
with the quantity Zentail now intends to send. It is a level check rather than a one-shot
event, so it takes no since and returns no cursor. Whatever the server-side cap cuts
off comes back on the next call.
{ "skus": ["TSHIRT-RED-M", "TSHIRT-RED-L"], "driftListings": [ { "sku": "TSHIRT-RED-M", "listingId": "7788" }, { "sku": "TSHIRT-RED-L", "listingId": "7788" } ]}driftListings pairs each SKU with its Zentail listingId, in the same order as skus.
Use it to group SKUs by listing, with two rules:
- A SKU whose listing cannot be resolved still appears, with an empty
listingId. Group those SKUs separately. Do not pool them under one empty key, which would treat them as a single listing. - A non-empty
skuswith an emptydriftListingsis an error, not “no work”. Do not skip those SKUs.
Two optional query parameters shape what comes back. Their defaults mean a SKU can be
absent for reasons other than “not drifting”. Both are durations written in seconds, such
as failureBackoff=3600s:
| Parameter | Default | What it does |
|---|---|---|
stableFor |
15 min | Only report a variant whose intended quantity has been unchanged for at least this long, so the check does not duplicate a push the poller already has in flight |
failureBackoff |
24 h | Leave a variant alone for this long after a failed submission attempt, which bounds the retry rate |
failureBackoff is the one to know about. A listing the channel keeps rejecting
re-reports once a day, not every cycle, so a SKU you are actively trying to fix can
drop out of the response for 24 hours. Lower it if you are working through a backlog.
3. Report what you sent
Section titled “3. Report what you sent”The drift check depends on you telling Zentail what you sent. After each inventory push,
call
POST /v2/storefront/listing/submission/{submissionId}/inventory:
{ "inventoryLevelSent": "100", "successful": true }inventoryLevelSent is a 64-bit integer, so it is a JSON string. Zentail assigns the
submissionId. You get it from the
CreateSubmissions call
that opened the submission, not from anything in your own system. Without these reports
Zentail has nothing to compare against, and the drift check stays quiet.
Next steps
Section titled “Next steps”Pricing works much the same way, so tackle it next.
See also
Section titled “See also”API changelog · Built 0c509dd3