Inventory
Adjust stock with a reason; movements are logged.
The dedicated inventory endpoint exists so every stock change has an audit
trail: who, what, why, when. Direct stock writes through PATCH /v1/admin/products/{id} bypass that log — use them for initial seeding
only.
Adjust stock
POST /api/v1/admin/inventory/adjustScope: inventory:write
Request body
{
"productId": "f3a7e9b8-...",
"variantId": "v9d8c7b6-...",
"delta": -3,
"reason": "stripe:ch_3MyChargeId fulfillment"
}| Field | Type | Required | Notes |
|---|---|---|---|
productId | uuid | yes | Product to adjust. |
variantId | uuid | no | Required if the product has variants. |
delta | integer | yes | Signed. -3 decrements by 3; +10 increments by 10. |
reason | string | yes | 1–200 chars. Free-form; include an external ID for idempotency. |
Response
HTTP/1.1 201 Created
{
"productId": "f3a7e9b8-...",
"variantId": "v9d8c7b6-...",
"previousStock": 7,
"newStock": 4,
"movementId": "mv_a1b2c3...",
"createdAt": "2026-05-27T14:00:00.000Z"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_body | Validation failed. |
| 404 | not_found | Product or variant doesn't exist in this shop. |
| 409 | stock_underflow | Resulting stock would go below zero. |
stock_underflow is the safe failure mode. We never let stock go negative
via this endpoint. If you need to record a sale that overdrew stock (e.g. a
race with a separate POS), use POST /v1/admin/products/{id} with an
explicit stock value and tag the reason on a follow-up adjustment.
Atomicity
delta is applied atomically in a single transaction. Concurrent calls
serialise; you won't see lost updates.
Audit log
Each adjustment writes an InventoryAdjustment row with apiKeyId,
createdAt, delta, reason, previousStock, newStock. The log is
visible in the panel (Inventory → Movements) and will be exposed via a
dedicated GET /v1/admin/inventory/movements endpoint in v1.1.