Inventory

Flint tracks stock per location and holds it with real claims, not a counter it decrements and hopes for the best. This guide follows one mug from arriving at a warehouse to leaving on a shipment, including multi-location routing and reservation lifecycle.

The Inventory API reference has the full resource and quantity model. Start here if you want the shape of a working integration first.

The model in one paragraph#

An inventory item is the thing you stock. A Location is where you stock it. An inventory level is the pair: this item, at this location, with its quantities. A catalog variant points at an inventory item rather than holding a count, so one physical stock pool can back several sellable products. Everything else here either changes a level or explains a change.

1. Make a Location that can hold stock#

Creating a Location is not enough. A Location participates in allocation only once it carries an inventory block.

Bash
curl -X POST https://api.withflintpay.com/v1/locations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Brooklyn warehouse",
    "status": "active",
    "timezone": "America/New_York",
    "address": {
      "line1": "120 Kent Avenue",
      "city": "Brooklyn",
      "state": "NY",
      "postal_code": "11249",
      "country": "US"
    },
    "inventory": { "allocation_status": "active" }
  }'

Creating the Location and its inventory block together needs both merchants.locations.write and commerce.inventory_locations.write. To enable inventory on a Location that already exists, PUT /v1/locations/{location_id}/inventory with expected_inventory_revision: null.

2. Make a variant that consumes stock#

Tracking is never inferred. A variant declares it, and a variant you do not mark tracked sells without a stock check even if it is a physical good.

JSON
{
  "name": "Ceramic mug",
  "variants": [
    {
      "unit_price_money": { "amount": 1800, "currency": "USD" },
      "inventory_tracking": "tracked",
      "inventory_item": { "name": "Ceramic mug, 12oz", "sku": "mug-12-white" }
    }
  ]
}

Passing inventory_item inline creates the item and links it in the same write. Pass inventory_item_id instead to point at an item you already have, which is how two variants share one stock pool.

3. Put stock on the shelf#

Levels are never written directly. You record what happened and Flint derives the numbers.

Bash
curl -X POST https://api.withflintpay.com/v1/inventory-adjustments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: receiving-1842" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "received_stock",
    "occurred_at": "2026-07-21T14:18:00Z",
    "source_system": { "type": "wms", "external_source_id": "east-coast-wms" },
    "lines": [{
      "inventory_item_id": "invi_...",
      "location_id": "loc_...",
      "on_hand_quantity_delta": 25
    }]
  }'

Three things about this request generalize to every quantity command:

  • The Idempotency-Key header is required. Omit it and you get 400 IDEMPOTENCY_KEY_REQUIRED. Flint will not generate one, because a generated key cannot protect the retry that matters: the one where your first request succeeded and you never saw the response. See the Idempotency guide.
  • occurred_at is when the stock actually moved, not when you told us. Required for pos, wms, erp, and other sources. A warehouse feed that batches overnight still produces an honest audit trail.
  • The response carries resulting_inventory_levels, the full post-commit state of every level touched. No follow-up read, and a replay returns the levels as they were when the command committed.

The reason constrains what the deltas may do, so the ledger cannot carry a misleading label: received_stock requires a positive on-hand delta, damage moves units into the damaged bucket without changing on-hand, theft and loss require a negative on-hand delta, and condition_changed moves units between conditions while on-hand stays put.

4. Sell it#

For an order, set the routing source and let payment do the rest.

JSON
{
  "line_items": [{ "variant_id": "var_...", "quantity": 2 }],
  "inventory_routing_source": { "type": "fixed_location", "location_id": "loc_..." }
}

When you pay the order, Flint holds the stock, commits it on success, and releases it if the payment fails. An order with tracked line items and no routing source cannot hold stock: paying it returns INVENTORY_ROUTING_SOURCE_REQUIRED. If nothing can serve the demand, payment fails with INVENTORY_UNAVAILABLE before any money moves.

Two order fields are worth watching:

FieldMeaning
inventory_reservation_idThe claim holding stock for this order
inventory_exception_statuspaid_inventory_failed when payment succeeded but stock could not be committed

An exception is resolved with POST /v1/orders/{order_id}/inventory-exception/resolve, or automatically refunded if you set post_payment_inventory_failure_action to auto_refund in settings. It also raises order.inventory_exception.created.

5. Ship it#

Committed stock is still physically present. It leaves on_hand_quantity when fulfillment hands the goods off: completing a shipment, a buyer collecting a pickup, or a courier taking a local delivery. Canceling before that point releases the claim with no physical movement. Refunding money never restocks anything on its own.

Putting returned stock back#

A returned unit re-enters stock when you record what arrived and what condition it is in, never as a side effect of a refund.

When merchandise comes back through a Flint Return, record the disposition on the Return. Flint writes the inventory receipt for you at the location you name and links it to the return line, and the Return caps the quantity you can disposition. The disposition type picks the bucket: sellable restocks available stock, quality_control, damaged, and quarantined land on hand but unsellable, and lost records that the unit never arrived and moves no stock.

When stock comes back outside a Flint Return, post the receipt directly. POST /v1/inventory-return-receipts records the arrival and its disposition in one command.

JSON
{
  "source_system": { "type": "wms", "external_source_id": "wms-eu-1" },
  "lines": [
    {
      "inventory_item_id": "invi_1kmn0aExample",
      "receiving_location_id": "loc_1kmn0aExample",
      "quantity": 1,
      "disposition": "sellable",
      "inventory_reservation_id": "invr_1kmn0aExample",
      "inventory_reservation_line_id": "invrln_1kmn0aExample"
    }
  ]
}

Pair inventory_reservation_id with inventory_reservation_line_id to have Flint check the quantity against what that order actually consumed, so a receipt cannot restock more than went out. Without that pair, nothing bounds the receipt.

A positive adjustment is still the right tool for corrections that are not returns, such as found stock or a cycle-count fix.

Holding stock outside a Flint order#

If you run your own cart or POS, reserve directly. POST /v1/inventory-reservations routes and holds in one atomic operation.

JSON
{
  "type": "standalone",
  "owner": { "type": "merchant", "key": "cart_1842", "expires_at": "2026-07-21T14:33:00Z" },
  "inventory_routing_source": { "type": "fixed_location", "location_id": "loc_..." },
  "demands": [{
    "demand_key": "cart_line_1",
    "inventory_item_id": "invi_...",
    "quantity": 2,
    "splitting_behavior": "single_location"
  }]
}

owner.key is your identifier for whatever holds the stock, and there is one active reservation per key. owner.expires_at is required and can be at most 15 minutes out; if the buyer needs longer at the payment step, call start-payment-window once for up to 1800 more seconds. When a deadline passes, the remaining hold is released and any committed quantity survives.

From there, quantity moves forward with cumulative targets, never deltas:

StepCallLine field
Payment confirmedPOST .../committarget_committed_quantity
Goods handed offPOST .../consumetarget_consumed_quantity
Buyer abandoned the cartPOST .../releasetarget_released_from_held_quantity

Cumulative means "this is how much should be committed by now," so resending a target you already applied succeeds and changes nothing. Each call takes expected_inventory_reservation_revision, so a stale caller is rejected rather than silently overwriting. consume also requires provenance; commit and release reject it.

Before you hold anything, POST /v1/inventory-availability-previews answers "could this work" without claiming stock, which is what a product page wants. It is advisory: only a reservation prevents overselling, and stock can change between the preview and the claim.

Selling from more than one location#

With several locations, replace fixed_location with an allocation policy. A policy version ranks locations into priority groups and says whether an order may be split.

JSON
{
  "name": "US retail routing",
  "status": "active",
  "version": {
    "location_groups": [
      { "location_id": "loc_brooklyn", "group_priority": 1 },
      { "location_id": "loc_oakland", "group_priority": 2 }
    ],
    "within_group_order": "location_id_ascending",
    "splitting_behavior": "split_when_required",
    "maximum_locations_per_assignment": 3
  }
}

Routing is deterministic and split-averse:

  1. Eligible locations are ordered by group priority, then by location ID.
  2. If any single location can serve the whole request, it takes all of it.
  3. Otherwise, with split_when_required, Flint walks that order taking as much as each location can serve, up to maximum_locations_per_assignment.

single_location fails rather than splitting. Constraints only ever narrow: a line that must ship whole stays whole even under split_when_required.

Publishing a new version affects future routing only. Reservations and quotes keep the version they pinned, so republishing a policy never reroutes stock that is already claimed. POST /v1/inventory-routing-previews shows the assignment a request would produce without holding anything.

Staying in sync#

Subscribe to inventory.level.updated for quantity changes, inventory.reservation.* for claim lifecycle, and inventory.shortage.detected plus inventory.action_required for the cases that need a human. Events fire for every qualifying change including ones your own commands caused, so deduplicate by event ID rather than trying to filter your own writes. The event catalog lists all eleven inventory events.

Common errors#

CodeWhat happenedWhat to do
IDEMPOTENCY_KEY_REQUIREDA quantity command arrived with no keySend Idempotency-Key and reuse it on retries
INVENTORY_INSUFFICIENTNo complete assignment could satisfy the demandReplenish, widen the policy, or reduce the quantity
INVENTORY_CHANGEDA revision or expected quantity was staleRe-read, then retry with the current basis
INVENTORY_RESERVATION_EXISTSThe owner key already has an active reservationReuse or release the existing one
INVENTORY_SHORTAGEThe committed quantity is not backed by physical stockReplenish, reallocate, or cancel
INVENTORY_ROUTING_SOURCE_REQUIREDTracked demand with nowhere to route itSet inventory_routing_source on the order or request

Routing conflicts include bounded per-line details when Flint can identify the affected demand. Other 409 responses identify the stale basis through their error code and parameter. The full list is in the error reference.

Rate this doc