Skip to content

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.

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.

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 inventory object and the quantity inside each entry of merchantFulfillableWarehouseQuantities. All four are 64-bit integers, which JSON numbers cannot carry safely. Parse them as integers.
  • totalQuantity is usually all you need. It is the quantity available for sale now, after pending orders across all of the seller’s channels. It should equal merchantFulfillableQuantity plus storefrontFulfillableQuantity, and the schema states it that way rather than as a guarantee. Treat totalQuantity as authoritative, and do not reject a payload whose parts disagree with it.
  • updatedAt is 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}.

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.

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 skus with an empty driftListings is 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.

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.

Pricing works much the same way, so tackle it next.

API changelog · Built 0c509dd3