Skip to content

Inventory movements

The append-only movement model, writing a movement, the movement event, and why absolute writes are forbidden.

Updated 2 Sept 20262 min read
On this page

This page is for anyone syncing stock or recording damage and returns. Inventory in Dukkan is an append-only ledger of movements; there is no stock number you write, only movements you add, from which the server computes the balance.

The model#

movement-object.jsonJSON
{
  "id": "0a1b2c3d-0001-4e8f-9b70-2d1c3e4f5a6b",
  "sequence": 418,
  "variant_id": "c0ffee00-0001-4e8f-9b70-2d1c3e4f5a6b",
  "location_id": "10c00000-0001-4e8f-9b70-2d1c3e4f5a6b",
  "delta": -2,
  "stock_after": 23,
  "reason": "damage",
  "occurred_at": "2026-08-18T16:10:00.000Z"
}
Field Meaning
sequence per-store monotonic counter; order movements by it
variant_id the variant; may be null for movements tied to a deleted variant
location_id the location (every store has a default location and possibly warehouses)
delta the signed change; never zero
stock_after the balance at this location after the movement, as computed by the server
reason why the movement happened
occurred_at RFC 3339 timestamp

Reasons an app may write: correction, initial, return and damage. Reads also show reasons the platform writes (sales, cancellations, transfers); treat reason as an open string when reading.

Why absolute writes are forbidden#

Writing "stock = 40" erases whatever happened between your read and your write: a storefront sale, a POS sale, another app. A movement of delta: +5 with reason correction composes safely with everything before it and leaves a trail the merchant can audit. That is why the API never accepts an absolute number.

Write a movement#

Requires inventory:write and an Idempotency-Key:

Shell
curl -X POST https://dukkan.one/platform-api/v1/inventory/movements \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Idempotency-Key: damage-SHIPMENT_ID-1" \
  -H "Content-Type: application/json" \
  -d @movement.json
inventory-movement.jsonJSON
{
  "variant_id": "c0ffee00-0001-4e8f-9b70-2d1c3e4f5a6b",
  "location_id": "10c00000-0001-4e8f-9b70-2d1c3e4f5a6b",
  "delta": -2,
  "reason": "damage",
  "note": "Broken in transit"
}
  • delta is between −1,000,000 and 1,000,000 and never zero.
  • reason is one of correction, initial, return, damage.
  • note is optional, up to 500 characters.
  • The 201 response carries the recorded movement; stock_after appears in it only when the install also holds inventory:read.
  • A movement that would take available stock below the reserved quantity returns 409, as does a movement on a product without inventory tracking.

Read movements#

Shell
curl "https://dukkan.one/platform-api/v1/inventory/movements?limit=100" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Requires inventory:read. The list pages with an opaque cursor; for incremental sync keep the last sequence you processed and skip anything at or below it.

The inventory.movement_created event#

Delivered for every movement, from any source, to subscriptions holding inventory:read:

event-inventory-movement.jsonJSON
{
  "movement_id": "0a1b2c3d-0001-4e8f-9b70-2d1c3e4f5a6b",
  "variant_id": "c0ffee00-0001-4e8f-9b70-2d1c3e4f5a6b",
  "location_id": "10c00000-0001-4e8f-9b70-2d1c3e4f5a6b",
  "delta": -2,
  "stock_after": 23,
  "reason": "damage"
}

The event carries stock_after directly, so you need no extra call for the balance at that location. Order events by the envelope sequence as described in Webhooks.

Restock on refund#

When you refund specific lines, you can return the quantities to stock in the same operation with the restock: { "location_id": "…" } option of POST /orders/{id}/refunds. It requires lines in the refund and the inventory:write scope; products without inventory tracking are skipped silently. Details in Money in minor units.