Inventory tracks how much of a thing you have, where it is, and what is already promised to someone else.
Two resources carry the state. An inventory item is the thing you stock; a catalog variant points at one rather than carrying its own counter, so the same physical stock can back several sellable products. An inventory level is one inventory item at one Location, and it is where quantities actually live. Every other resource here either changes a level or explains a change.
New to this surface? The Inventory guide walks one item from first stock through a paid, fulfilled order.
Quantity states#
A level does not have a single count. It has physical stock, claims against that stock, and a derived number you can still sell.
| Quantity | Meaning |
|---|---|
on_hand_quantity | Physically present, in any condition |
quality_control_quantity, damaged_quantity, quarantined_quantity | Present but not sellable |
held_quantity | Claimed by an open reservation, not yet paid |
committed_quantity | Claimed and confirmed, not yet shipped |
safety_stock_quantity | Deliberately withheld from sale |
available_quantity | Derived: what you can still sell |
shortage_quantity | Derived: claims exceeding sellable stock |
available_quantity and shortage_quantity are computed, never written. You change stock by recording what happened, and Flint derives the rest.
Committed stock is still physically present. It leaves on_hand_quantity only when fulfillment consumes it, which is why canceling before shipment restores availability without any physical movement.
The three write semantics#
Every command that changes a quantity uses exactly one semantic, and its field names tell you which:
- Relative delta (
*_quantity_delta) for adjustments. "Twenty-five more arrived." - Absolute value (
target_safety_stock_quantity) for safety stock. "Keep five units unavailable for sale." - Cumulative target (
target_*_quantity) for reservation transitions. "Two of these should be committed by now."
Cumulative targets converge: resending a target you already applied succeeds and changes nothing. A bare quantity field never appears on a converging transition, so a target can't be mistaken for a delta.
What a quantity command returns#
Every command that changes a quantity returns an operation-specific result object with the same effect fields: the echoed idempotency_key, the inventory_movement_ids it produced, and resulting_inventory_levels, a complete post-commit projection of every level it touched. Commands backed by a resource also include that created or updated resource under its documented field. You do not need a follow-up read to learn where the numbers landed, and a replay returns the levels as of the original command rather than whatever they are now.
Inventory list page_token values are opaque and bound to the route and filters that produced them. Reuse the token unchanged with the same filters; changing a filter requires starting from the first page. Scalar filters accept one value, not repeated or comma-joined alternatives.
Idempotency is required, not optional#
Every command that can change a quantity rejects a request with no Idempotency-Key, returning 400 IDEMPOTENCY_KEY_REQUIRED. Flint will not generate one for you, because a generated key cannot protect the case that actually matters: your request succeeded, the response never reached you, and you retry.
Reusing a key with identical input returns the original result, including the original level projections rather than whatever the quantities happen to be now. Reusing it with different input is a conflict. This holds for far longer than ordinary HTTP idempotency retention, so a delayed webhook or warehouse replay from last week still cannot double-count.
Concurrency#
Anything with a mutable basis takes an expected revision and rejects a stale one rather than overwriting a concurrent change: expected_inventory_item_revision, expected_inventory_allocation_policy_revision, expected_inventory_reservation_revision, and expected_safety_stock_quantity on safety stock lines. Read, then send back what you read.
Reserving stock#
POST /v1/inventory-reservations routes demand and holds stock in one atomic operation. You give it demand, a routing source, and an owner; it decides which Locations serve the demand and holds the quantity there. It is all or nothing: if the whole request cannot be satisfied, nothing is held and the response says what fell short.
The owner names what holds the stock (key, your own cart or session identifier) and when the claim lapses (expires_at, required, at most 15 minutes out). One active reservation per key. If a buyer needs longer to pay, POST /v1/inventory-reservations/{id}/start-payment-window replaces the deadline once with a window of up to 1800 seconds; at that deadline the remaining hold is released while committed and consumed quantity survive.
Held quantity moves forward through commit, then consume, and can leave through release or reallocation. Consumed and released quantities are terminal; nothing returns to held. release names the bucket it drains (target_released_from_held_quantity or target_released_from_committed_quantity), so no priority rule decides for you.
Availability checks and routing previews answer "what would happen" without holding anything. They are advisory by construction: only a reservation prevents overselling.
Orders and checkout#
Selling through a Flint order does not require calling any of this yourself. Set inventory_routing_source on the order and Flint holds stock when payment begins, commits it when payment succeeds, releases it when payment fails, and consumes it when fulfillment hands the goods off. The order carries inventory_reservation_id for the claim and inventory_demand_revision, which advances whenever tracked demand or routing changes.
An order with tracked line items and no routing source cannot hold stock: paying it returns INVENTORY_ROUTING_SOURCE_REQUIRED. If no location can serve the demand, payment fails with INVENTORY_UNAVAILABLE before money moves.
When payment succeeds but stock cannot be committed, the order reports inventory_exception_status: "paid_inventory_failed" and emits order.inventory_exception.created. Resolve it with POST /v1/orders/{order_id}/inventory-exception/resolve, or set post_payment_inventory_failure_action to auto_refund in settings to have Flint refund instead.
Reserve directly only when the cart lives outside Flint: your own storefront, a POS, or a system that holds stock before an order exists.
Routing across locations#
The routing source decides which Locations can serve demand, and every request that creates tracked demand carries exactly one:
{"type": "fixed_location", "location_id": "loc_..."}sends everything to one Location. Single-site merchants never need a policy.{"type": "policy", "inventory_allocation_policy_id": "invp_..."}resolves the policy's current version when the request is made.{"type": "policy_version", "inventory_allocation_policy_version_id": "invpv_..."}pins an exact version.
A policy version ranks Locations into priority groups. Routing is deterministic and prefers to keep an order together:
- Eligible Locations are ordered by ascending group priority, then ascending location ID.
- If any single eligible Location can satisfy the whole request, it takes all of it.
- Otherwise, if
splitting_behaviorissplit_when_required, Flint walks that same order and takes as much as each Location can serve, up tomaximum_locations_per_assignment.
single_location fails instead of splitting. Per-demand and fulfillment constraints can narrow this further but never widen it: the effective rule is the intersection, so a line that must ship whole stays whole even under split_when_required.
Publishing a new policy version affects future routing. Existing reservations and quotes keep the version they pinned.
When stock is promised but missing#
A loss, damage adjustment, or other physical correction can leave more claims than sellable stock. Flint does not pretend otherwise. It protects claims deterministically (committed before held, oldest first) and reports the remainder as at_risk_held_quantity and at_risk_committed_quantity on the reservation lines, alongside inventory.shortage.detected and inventory.reservation.at_risk events.
At-risk quantity cannot be consumed. The reservation exposes an inventory_action_required object naming the affected lines, and payment dispatch refuses to start against an at-risk hold rather than taking money it cannot honor. Replenish with an adjustment or release the affected claim before retrying commerce.
Everything else is an explanation#
Adjustments and reservation transitions ensure that every change to a level has a durable reason attached. Each one produces inventory movements, an append-only record of what changed and what the level looked like afterward. When a number is wrong, the movement history is how you find out why, which is why physical commands carry occurred_at and source-system provenance: the moment stock actually moved, not the moment your system got around to telling us.
