---
title: "Set absolute inventory levels by (product, variant, outlet) tuple"
method: POST
path: "/v1/inventories/set-levels"
tags: ["Inventories"]
---

# Set absolute inventory levels by (product, variant, outlet) tuple

`POST /v1/inventories/set-levels`

## Request

- Base URL: `https://api.finalpos.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.finalpos.com/v1/inventories/set-levels`
- Auth: API key in header `x-api-key`

Upserts absolute on-hand quantities for a list of (productId, variantId, outletId) tuples scoped to the API-key company. Existing live rows are overwritten; missing rows are created. Writes exactly the tuples supplied — no outlet fan-out, no zero-fill. Does NOT revive soft-deleted rows (a soft-deleted tuple is re-inserted as a new live row). Records StockChange RECOUNT history for managed tuples whose quantity changed.

## Request body

- SetInventoryLevelsDto
  - `items` InventoryLevelItemDto[], required: Inventory levels to set (upsert by productId + variantId + outletId)
    - `productId` string, required: Product ID
    - `variantId` string, required: Variant ID
    - `outletId` string, required: Outlet ID
    - `quantity` number, required: Absolute on-hand quantity to set for this tuple. May be negative — on-hand stock legally goes negative via external oversell (e.g. Shopify available after an oversold order).
    - `manageStock` boolean, required: Whether the tuple manages finite stock (false = untracked/infinite)
    - `expectedQuantity` number: Compare-and-set guard: apply this item iff the tuple currently holds exactly this quantity. A missing inventory row counts as current 0 (so expectedQuantity 0 creates the row). May be negative — on-hand stock legally goes negative via external sales. When any item in the batch carries expectedQuantity or idempotencyKey, the response additionally includes per-item `results`. Requires routed inventory writes; omit for the unconditional legacy behaviour.
    - `idempotencyKey` string: Caller-supplied key for safe retries, stamped verbatim on the stock-change ledger row this write emits. A replayed key returns per-item status "duplicate" without re-applying.

## Response `200`

- SetInventoryLevelsResponseDto
  - `statusCode` number, required: HTTP status code
  - `message` string, required: Result message
  - `matched` number, required: Existing tuples matched by (productId, variantId, outletId)
  - `upserted` number, required: New inventory rows inserted
  - `modified` number, required: Existing rows whose quantity/manageStock changed
  - `results` SetInventoryLevelsResultDto[]: Per-item outcomes. Present ONLY when the request used expectedQuantity or idempotencyKey on at least one item; requests without those fields keep the legacy aggregate-only response.
    - `variantId` string, required: Variant ID of the tuple
    - `outletId` string, required: Outlet ID of the tuple
    - `status` 'applied' | 'stale-lost-race' | 'duplicate' | 'failed', required: "stale-lost-race" means the expectedQuantity guard did not match the live tuple: nothing was written and no stock-change was emitted. It is a first-class outcome of a compare-and-set, not an error — the batch still returns HTTP 200.
    - `updatedQuantity` number: Post-write on-hand quantity (applied / duplicate only)
    - `error` string: Failure detail (failed only)

## Other responses

- `400`

---

[API](https://skmtc.dev/finalpos/apis/final-pos-public-api.md) · [All operations](https://skmtc.dev/finalpos/apis/final-pos-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/finalpos/apis/final-pos-public-api/revisions/6a6842dcb479?raw)
