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 carries the quantities. 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 (
safety_stock_quantity) for safety stock. "Keep five units unavailable for sale." - Cumulative target (
target_*_quantity) for reservation and transfer transitions. "Two of these should be committed or received 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.
Durable inventory commands require idempotency#
Inventory commands that create durable workflow state or can change a quantity reject a request with no Idempotency-Key, returning 400 IDEMPOTENCY_KEY_REQUIRED. This includes quantity commands, the full count workflow, and transfer creation and transitions. Flint will not generate a key for you, because a key created after the request arrives cannot protect a retry when the first response never reached you.
Keys are scoped to the authenticated merchant, environment, and endpoint. The request fingerprint includes the canonical request body. Reusing a key with identical input returns the original response, including the original level projections rather than current quantities. Reusing it with different input is a conflict. Flint retains the key for at least as long as the command's inventory effects. The response returns the key, and the relevant resource or movement list provides the recovery path after an unacknowledged request.
Concurrency#
Inventory PATCH requests require expected_version and reject a stale version rather than overwriting a concurrent change. Count apply and cancel requests and transfer transitions accept expected_version optionally and enforce it when present. Read an item or allocation policy by ID, or another inventory resource from its list operation, and send the version when you need the command to fail if the resource changed.
Reserving stock#
Creating, committing, consuming, and releasing inventory reservations requires commerce.inventory.write. Managing a Location's inventory settings requires commerce.inventory_locations.write, so you can grant stock operations without permission to change where orders ship from or buyers pick up.
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 is allowed per key. When the deadline passes, the remaining hold is released while committed and consumed quantity survive.
Held quantity moves forward through commit, then consume, and can leave through release. 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.
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.
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 configuration when the request is made.{"type": "policy_version", "inventory_allocation_policy_version_id": "invpv_..."}pins an immutable configuration snapshot already named by Flint evidence.
An allocation policy's configuration ranks Locations into priority groups. Send it when creating the policy or replace it with PATCH /v1/inventory-allocation-policies/{inventory_allocation_policy_id}. PATCH requires the current expected_version and commits the policy fields and configuration together.
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.
Changing policy configuration affects future routing. Existing reservations and quotes keep the immutable snapshot 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.
Operations#
Inventory items are managed with POST /v1/inventory-items, GET /v1/inventory-items, GET /v1/inventory-items/{inventory_item_id}, PATCH /v1/inventory-items/{inventory_item_id}, and DELETE /v1/inventory-items/{inventory_item_id}. These operations create, list, retrieve, update, and archive stock identities.
Inventory levels use GET /v1/inventory-levels to list current quantities and versions. Send expand=inventory_item to render each level's inventory item beside inventory_item_id, so a stock table does not need a read per row; the whole page resolves in one lookup. Filter the list with query, a case-insensitive substring match on the inventory item's name, SKU, and barcode that also matches an inventory item ID from its start (invi_01M2); with min_available_quantity and max_available_quantity, which are inclusive bounds on available_quantity; and with inventory_item_status, which accepts active, inactive, or archived. Only stock behind an active item can be routed or reserved, so inventory_item_status=active is the list of what you can sell. PATCH /v1/inventory-levels/{inventory_level_id} changes one level's safety stock using expected_version and safety_stock_quantity.
The ledger uses GET /v1/inventory-movements to list immutable quantity changes, oldest recorded first; send order=desc to read the newest first. It accepts the same expand=inventory_item the level list accepts, so a history table does not need a read per row. Adjustments use POST /v1/inventory-adjustments to record a physical correction and GET /v1/inventory-adjustments to list those commands.
Allocation policies use POST /v1/inventory-allocation-policies, GET /v1/inventory-allocation-policies, GET /v1/inventory-allocation-policies/{inventory_allocation_policy_id}, PATCH /v1/inventory-allocation-policies/{inventory_allocation_policy_id}, and DELETE /v1/inventory-allocation-policies/{inventory_allocation_policy_id} to create, list, retrieve, update, and archive routing rules.
Reservations use POST /v1/inventory-reservations to route and hold stock and GET /v1/inventory-reservations to list current and past claims. POST /v1/inventory-reservations/{inventory_reservation_id}/commit, POST /v1/inventory-reservations/{inventory_reservation_id}/consume, and POST /v1/inventory-reservations/{inventory_reservation_id}/release advance cumulative line targets.
Receipts use POST /v1/inventory-receipts to record returned stock and GET /v1/inventory-receipts to list receipt history.
Counts use POST /v1/inventory-counts to start a count and GET /v1/inventory-counts to list counts. PATCH /v1/inventory-counts/{inventory_count_id} atomically replaces the observation set using expected_version. POST /v1/inventory-counts/{inventory_count_id}/apply turns the differences into movements, while POST /v1/inventory-counts/{inventory_count_id}/cancel closes the count without changing stock. While a count is unapplied, every line carries expected_on_hand_quantity, expected_quality_control_quantity, expected_damaged_quantity, and expected_quarantined_quantity: the quantities Flint holds for that item at that location right now, so a counting screen can show what it expects before anyone writes a number down. Applying the count sets the level to the counted quantities, and the apply is rejected if the level changed since the line was captured. An applied line drops the expected quantities and reports on_hand_variance, quality_control_variance, damaged_variance, and quarantined_variance: the difference the apply made. A canceled count reports neither.
Transfers use POST /v1/inventory-transfers to create a transfer, GET /v1/inventory-transfers to list transfers, and PATCH /v1/inventory-transfers/{inventory_transfer_id} to edit an open transfer. POST /v1/inventory-transfers/{inventory_transfer_id}/transitions accepts one of the actions in the transfer's supported_actions: depart, receive, return_to_origin, report_loss, or cancel. Each line sends a cumulative target for the selected action. An unavailable action returns 409 INVENTORY_TRANSFER_ACTION_NOT_ALLOWED with the current transfer status, version, and supported actions.
Retrieve items and allocation policies#
Use GET /v1/inventory-items/{inventory_item_id} when you have an item ID from a catalog variant or past inventory operation. Use GET /v1/inventory-allocation-policies/{inventory_allocation_policy_id} to read a policy's current routing configuration and version before editing it. Both return the resource in data, with status and version, including when it is inactive or archived. An unknown ID or an ID belonging to another merchant or environment returns 404.
curl "https://api.withflintpay.com/v1/inventory-items/invi_01K0P7W6A4N9F3J2T8Q5R1C6XM" \
-H "Authorization: Bearer YOUR_API_KEY"
curl "https://api.withflintpay.com/v1/inventory-allocation-policies/invp_01K0P7W6A4N9F3J2T8Q5R1C6XM" \
-H "Authorization: Bearer YOUR_API_KEY"
DELETE archives an item or allocation policy. You can still retrieve it by ID, but the default list includes only active and inactive resources. Send status=archived to list archived resources:
curl "https://api.withflintpay.com/v1/inventory-items?status=archived" \
-H "Authorization: Bearer YOUR_API_KEY"
curl "https://api.withflintpay.com/v1/inventory-allocation-policies?status=archived" \
-H "Authorization: Bearer YOUR_API_KEY"
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 moved, not the moment your system got around to telling us.
