Overview
Three endpoints support atomic multi-item inventory operations. Each accepts anitems[] array and guarantees all-or-nothing behavior: every item writes in a single database transaction. If item 5 of 10 fails, items 0-4 are automatically rolled back.
Each endpoint also accepts single-item (top-level) payloads for backward compatibility. All three need an API key with the
inventory:write scope; the read operations at the end of this guide need inventory:read.
POST /v1/inventory/receive
When to use
Use for non-PO standalone receipts — opening stock loads, vendor samples, or inventory counts that don’t correspond to a purchase order. Each item establishes its own FIFO cost layer and posts its own journal entry in the same transaction: debit to inventory, credit to the entity’s inventory adjustments suspense account. Passoffset_account_id at the top level of the body to credit a different account for the whole batch. For PO-linked receipts use POST /v1/purchase-orders/{id}/receive.
Multi-item atomic receive
Response shape
journal_entry_id on each transaction links the receipt to its ledger entry.
Required fields per item
The single-item form puts
product_id, location_id, quantity, unit_cost and notes at the top level of the body instead of inside items[].
Validation error shape (400)
If any item fails upfront validation, no items are written and the response includes exactparam paths:
Products that cannot be received (422)
Receive, adjustments and transfers all refuse a product that does not keep stock: a kit, a service, a variant parent, a box, or any product with inventory tracking turned off. The call returns422 product_not_receivable with one entry per refused item in errors[], each carrying the index, the product_id and a hint; an item whose product does not exist is reported with the code product_not_found. Nothing is written.
If the batch fails after validation
A failure inside the transaction rolls back every item. A receive failure returnsreceive_failed with items_completed_before_failure, an informational count only, since all items are reverted. The batch also fails and rolls back if the entity has no inventory adjustments suspense account and you did not pass offset_account_id, because the entry has nothing to balance against.
POST /v1/inventory/adjustments
When to use
Use for cycle-count corrections, shrinkage write-downs, and inventory revaluations. A GL journal entry fires per item inside the same database transaction. Positive deltas (adding stock) should carry aunit_cost to establish a new FIFO layer; if you leave it out, Arcus uses the product’s existing average cost and refuses the item with unit_cost_required only when the product has no cost basis anywhere. Negative deltas consume existing FIFO layers automatically and calculate COGS from the oldest layers. A negative delta cannot take a balance below zero: it is refused with negative_balance_not_allowed unless the item sets allow_negative, which needs the inventory.allow_negative permission.
Multi-item atomic adjustment
Response shape
Required fields per item
GL behavior
Each adjustment item creates its own journal entry (DR/CR balanced to the cent):- Positive delta (add stock): DR Inventory Asset, CR Inventory Adjustment/Suspense. Amount =
quantity_delta * unit_cost. - Negative delta (remove stock): DR Inventory Adjustment/Shrinkage, CR Inventory Asset. Amount = FIFO COGS (oldest layers consumed first).
reason text: words like damage, theft, loss, shrinkage or spoilage post to shrinkage; cycle count or variance posts to count variance; obsolete, write-off or expired posts to inventory write-downs; revaluation or cost correction posts to revaluation; found stock, overage, recovered, data error or correction posts to overage income on an addition and to shrinkage on a removal; and a reason that matches none of these posts to the inventory adjustments suspense account. Underscores count as spaces, so cycle_count and found_stock match.
The journal_entry_id on each item links the transaction to the GL record. gl_amount is positive for additions, negative for removals.
Serialized product adjustments
For serialized products, include matching serial arrays whose length equalsabs(quantity_delta):
POST /v1/inventory/transfers
When to use
Use to move inventory between locations within the same entity (internal transfer) or between entities of the same organization (intercompany transfer, by addingentity_to_id). The call creates the transfer in draft status and moves no stock. Submit it with POST /v1/transfers/{id}/submit: an internal transfer ships immediately and posts its entry, and an intercompany transfer goes to pending approval first (approve, ship and receive follow on /v1/transfers/{id}/approve, /ship and /receive).
Multi-item atomic transfer
Response shape
availability_warnings lists every line whose quantity exceeds what the source location has available at the moment you create the draft (each entry has transfer_item_id, product_id, requested_qty, available_qty and shortfall). It does not block the draft, because you may be planning for stock that is still arriving. The block comes later: submitting or shipping a short line is refused with 400 insufficient_stock.
Required fields
A missing top-level field returns
400 with the code missing_param and the field in param; item problems return 400 multi_item_validation_failed with the same errors[] shape as the other two endpoints.
Field name aliases
Both naming conventions are accepted:GL lifecycle
Atomicity guarantee
All three endpoints wrap theiritems[] loop in a single pool.connect() + BEGIN/COMMIT/ROLLBACK transaction:
- All items validate upfront (before
BEGIN). BEGINstarts.- Each item writes inside the transaction.
- Any item failure triggers
ROLLBACK— every item reverts. COMMITfires only after all items succeed.
failed_item_index and items_completed_before_failure so you know which item triggered the rollback. A receive failure carries items_completed_before_failure only. All items are reverted regardless of that count; it is informational only.
Idempotency
All three endpoints honor theIdempotency-Key header. Replaying the same key within 24 hours returns the original response without re-applying the operation, and reusing a key with a different body is refused with 409 idempotency_key_collision. Use a UUID or a deterministic key tied to your batch (e.g. receive-batch-${date}-${batchId}).
Related endpoints
POST /v1/purchase-orders/{id}/receive— PO-linked receipts (triggers GRNI GL, matches against PO lines). Needs thepurchasing:writescope and a receiving location, sent as theX-Location-Idheader orlocation_idin the body. Partial receipts are supported; a receipt past the entity’s over-receipt tolerance is refused with400 over_receipt_blocked, and an identical retry inside a short window returns the original receipt withidempotent: true.GET /v1/inventory/balances— on-hand, allocated and available quantity per product and location, including products with zero on hand. Filter withproduct_id,location_id,variant_idorlow_stock_only; paginate withpageandper_page(default 25).GET /v1/inventory/transactions— movement history, newest first, with the quantity change, the on-hand before and after, and the source document. Filter byproduct_id,location_id,type,date_afterordate_before.GET /v1/inventory/fifo-layers— cost layers for one product in FIFO order, oldest first.product_idis required;location_idnarrows it.POST /v1/inventory/serials— bulk enroll up to 500 serial numbers with statusavailable(no on-hand change). A duplicate serial is skipped and reported in the per-rowerrors[]of a partial-success200response rather than failing the call. Enrolling serials never changes on-hand quantity, so receive the stock separately.

