Adjust stock#
This guide explains how stock is modelled in Afosto, how to read the current stock of a SKU, and which mutations change it — putting items back on stock, writing off items that turned out to be missing, reserving stock for a customer, and announcing goods that are on their way. It closes with the audit trail you use to verify that a change landed.
How stock is structured#
Stock in Afosto is not a single number you write to. It is derived from three layers:
- Inventory item (
InventoryItem) — one physical registration: a SKU, in a warehouse, at a position, with a quantity, a cost and a status (AVAILABLE,COMMITTED,RESERVED,UNAVAILABLE). Items with identical attributes are merged into the same line. - Inventory (
Inventory) — the summary of those items grouped per SKU, warehouse and position. This is what you read when you want "how many do I have", split out overquantity_available,quantity_committed,quantity_reserved,quantity_unavailableandquantity_expected. - Stock mutation (
StockMutation) — the audit record of every change that affected stock, with thesourcethat caused it and thenew_quantitythat resulted from it.
There is no setStock or updateStockLevel mutation. Stock is always the consequence of an
operation: receiving a supply, reserving items, allocating them to an order, restocking a return.
Pick the operation that matches what actually happened — the quantity follows. Stock that has no
operation behind it (an opening balance, or the outcome of a physical count) is written straight
to the inventory service over REST — see step 4.
Every change is labelled with a StockMutationSource:
| Source | Meaning |
|---|---|
REGISTER | New stock registered in a warehouse |
ALLOCATION | Stock committed to or released from an order |
RESTOCK | Items put back on stock after a cancellation or return |
CORRECTION | A correction — a count difference, or an item that was missing while picking |
EXTERNAL | A change pushed in by a connected external system |
UNKNOWN | Source could not be determined |
Overview#
- Read the current stock with the
inventoryquery - Inspect the individual inventory items with
inventoryItemsorinventoryAtLocation - Change the stock with the operation that matches what happened:
- Put order items back on stock —
restockOrderItems - Write off items that are missing —
markItemsAsMissing - Reserve stock for a customer —
createReservation - Announce incoming stock —
createAnnouncement,createSupply
- Put order items back on stock —
- Register or correct stock directly over REST when no operation sits behind the change
- Set the thresholds that drive reordering with
createItemPreferences/updateItemPreferences - Verify the result with the
stockMutationsquery
All of these require a valid API key — see Authentication.
Step 1 — Read the current stock#
The inventory query returns the stock summary, grouped per SKU, warehouse and position. Filter it by sku, warehouse_id or business_id.
Arguments:
| Name | Type | Required | Description |
|---|---|---|---|
sku | String | Optional | Limit the result to a single SKU. |
warehouse_id | String | Optional | Limit the result to one warehouse. |
business_id | String | Optional | Limit the result to the stock owned by one business. |
filters | InventoryFilterInput▾ | Optional | Richer filtering — `sku`, `warehouse_id`, `business_id`, `location_id`, `ids`, and `quantity` as an inequality filter. |
first | Int | Optional | Page size. |
after | String | Optional | Cursor to continue from — see [Pagination](/docs/graphql/pagination). |
Each node is an Inventory:
| Name | Type | Required | Description |
|---|---|---|---|
id | String! | Optional | Identifier of the grouped inventory line. |
ids | [String!]! | Optional | The IDs of the individual inventory items that make up this line. |
sku | String! | Optional | The SKU of the grouped items. |
quantity | Int64! | Optional | Total quantity on hand for this line. |
quantity_available | Int64! | Optional | Quantity that can still be sold — not committed, reserved or unavailable. |
quantity_committed | Int64! | Optional | Quantity allocated to an order and waiting to be picked. |
quantity_reserved | Int64! | Optional | Quantity held by a reservation or a claim. |
quantity_unavailable | Int64! | Optional | Quantity that is on hand but cannot be sold — damaged, quarantined, missing. |
quantity_expected | Int64! | Optional | Quantity announced as incoming but not yet received. |
warehouse | Warehouse | Optional | The warehouse the line is held in. |
position | Position | Optional | The position within the warehouse. |
cost | InventoryCost!▾ | Optional | Cost of the line. |
quantity is everything on hand; quantity_available is what you can still sell. A drop in
quantity_available without a drop in quantity means the goods are still there but have been
committed or reserved.
Step 2 — Inspect the individual inventory items#
The summary tells you how much there is. To see which registrations it is made of — batch numbers, serial numbers, expiry dates, per-item status — use inventoryItems.
Arguments:
| Name | Type | Required | Description |
|---|---|---|---|
filters | InventoryItemFilterInput!▾ | Required | At least one of the filters below. |
first | Int | Optional | Page size. |
after | String | Optional | Cursor to continue from. |
status is an InventoryItemState: AVAILABLE, COMMITTED, RESERVED or UNAVAILABLE. When an item is UNAVAILABLE, reason explains why.
To look at a single warehouse position instead — what a picker sees when they walk up to a shelf — use inventoryAtLocation:
Step 3 — Change the stock#
Which mutation you call depends on what happened to the goods.
| What happened | Mutation | Resulting source |
|---|---|---|
| A cancelled or returned item goes back on the shelf | restockOrderItems | RESTOCK |
| An item could not be found while picking | markItemsAsMissing | CORRECTION |
| An item is temporarily not sellable | markItemsAsUnavailable | CORRECTION |
| The item turned up again at a known location | markItemsAsAvailable | CORRECTION |
| Stock is held for a customer without an order yet | createReservation | ALLOCATION |
| Goods are on their way from a supplier | createAnnouncement → createSupply | REGISTER |
A — Put order items back on stock#
restockOrderItems takes items off a cancelled or returned order and puts their physical stock back into the inventory, at the location and position you choose. Use it as the final step of a cancellation or a return.
Input: RestockOrderItemsInput!
| Name | Type | Required | Description |
|---|---|---|---|
order_id | String! | Required | ID of the order the items belong to. |
items | [RestockItemInput!]!▾ | Required | The items to put back on stock. |
Returns: RestockOrderItemsPayload
Restocking only works for items that have been cancelled or returned. Restocking an item that is
still on an open, unshipped order is rejected — deallocate it with deallocateOrderItems instead,
which releases the commitment without moving stock.
B — Write off items that are missing#
When a picker cannot find an item, the recorded stock is higher than the physical stock. markItemsAsMissing records that difference: it takes the item off the order and writes a correction against the inventory.
Input: MarkItemsAsMissingInput!
| Name | Type | Required | Description |
|---|---|---|---|
order_id | String! | Required | ID of the order the items belong to. |
items | [MissingItemInput!]!▾ | Required | The items that could not be found. |
Returns: MarkItemsAsMissingPayload
Two related mutations cover the rest of the picking corrections:
markItemsAsUnavailable(MarkItemsAsUnavailableInput!, takesorder_idanditemswith anitem_id) — the goods exist but cannot be sold right now, for instance because they are damaged. The quantity stays inquantity, but moves out ofquantity_available.markItemsAsAvailable(MarkItemsAvailableInput!, takesorder_idanditemswith anitem_idand alocation_id) — the reverse: the item turned up at the given location and is sellable again.
C — Reserve stock for a customer#
A reservation holds quantity for a customer before there is an order — a click-and-collect hold, or goods set aside after a store visit. The reserved quantity leaves quantity_available and shows up in quantity_reserved, and the reservation expires automatically at expires_at.
Input: CreateReservationInput!
| Name | Type | Required | Description |
|---|---|---|---|
business_id | String! | Required | ID of the business the reserved items are claimed for. |
warehouse_id | String! | Required | ID of the warehouse the stock is held in. |
channel_id | String! | Required | ID of the channel the reservation is created for. |
customer | SetReservationContactInput!▾ | Required | The customer the stock is held for. |
items | [ReservationItemInput!]!▾ | Required | The items to reserve. |
expires_at | Int64! | Required | Unix timestamp (milliseconds) at which the reservation releases the stock again. |
notes | String | Optional | Free-form note on the reservation. |
id | String | Optional | Optional ID to assign to the reservation. |
Returns: CreateReservationPayload
Release the stock early with removeReservation:
D — Announce incoming stock#
Goods that a supplier is sending count as quantity_expected until they are received. Announce them in three steps: create the announcement, add its items, then group one or more announcements into a supply that your warehouse receives against.
Then add the items you expect to receive:
Finally, group the announcements into a supply. settings.processing decides what happens to the goods on arrival — REGISTER puts everything on stock, while the MATCH_* options cross-dock the goods straight onto waiting orders or purchases.
Input: CreateSupplyInput!
| Name | Type | Required | Description |
|---|---|---|---|
announcement_ids | [String!]! | Required | The announcements to receive in this supply. |
settings | SupplySettingsInput!▾ | Required | How the received goods are processed. |
location_id | String | Optional | Location the supply is received at. |
due_at | Int64 | Optional | Unix timestamp (milliseconds) the supply is due. |
id | String | Optional | Optional ID to assign to the supply. |
Returns: CreateSupplyPayload
If more arrives than was announced, book the difference on the supply with addOverage instead of creating a second announcement:
Step 4 — Register or correct stock directly#
Everything above changes stock as a side effect of an operation. When there is no operation — an opening balance for a new SKU, or the outcome of a physical count — you write to the inventory service directly. These three endpoints are REST, not GraphQL: there is no mutation that does this.
The query runner on this site only speaks GraphQL, so the examples below are curl. They use the
same API key as the GraphQL endpoint — see Authentication.
These endpoints take camelCase field names (warehouseId, batchNumber, serialNumber,
expiresAt), while the GraphQL API uses snake_case. Reading stock through inventory and writing
it through this endpoint means switching convention halfway.
Add stock#
POST /api/inventory/items registers new stock. Every posted item increases the quantity on a stock line — items with an identical set of attributes are merged into the same line, so posting the same SKU, warehouse, position and batch twice adds up rather than creating a second line.
Body: a data array of items.
| Name | Type | Required | Description |
|---|---|---|---|
sku | string | Required | The SKU to register stock for. |
warehouseId | string | Required | ID of the warehouse the stock is held in. |
businessId | string | Required | ID of the business that owns the stock. |
quantity | number | Optional | Number of items to register. |
state | string | Optional | `AVAILABLE` or `UNAVAILABLE`. Defaults to available. |
reason | string | Optional | `BROKEN` or `NONE` — only meaningful when `state` is `UNAVAILABLE`. |
position | string | Optional | Position within the warehouse. |
gtin | string[] | Optional | Scannable barcodes for the item. |
rfid | string | Optional | RFID value. |
cost | object▾ | Optional | Cost price of the item. |
batchNumber | string | Optional | Batch number. |
serialNumber | string | Optional | Serial number. |
weight | object▾ | Optional | Weight of the item. |
expiresAt | number | Optional | Unix timestamp (milliseconds) at which the item expires. |
A 201 returns the resulting stock lines:
Correct stock#
PATCH /api/inventory/items updates existing stock lines, identified by id. This is how you book the outcome of a count: set quantity to what you actually counted.
Body: a data array of updates. Each entry needs an id; every other field is optional and replaces the current value.
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Required | ID of the stock line to update — from `inventoryItems` or the `ids` on an `Inventory` node. |
quantity | number | Optional | The corrected quantity. |
state | string | Optional | `AVAILABLE` or `UNAVAILABLE`. |
reason | string | Optional | `BROKEN` — only meaningful when `state` is `UNAVAILABLE`. |
position | string | Optional | Move the line to another position. |
gtin | string[] | Optional | Scannable barcodes. |
rfid | string | Optional | RFID value. |
cost | object | Optional | Cost price — `amount` in cents and `currency`. |
batchNumber | string | Optional | Batch number. |
serialNumber | string | Optional | Serial number. |
weight | object | Optional | Weight — `amount` and `unit`. |
expiresAt | number | Optional | Unix timestamp (milliseconds) of expiry. |
Fetch the id first with the inventoryItems query from step
2. The ids field on an Inventory node lists
the individual lines behind a grouped summary, which is what you need when a SKU sits at several
positions.
Remove stock#
DELETE /api/inventory/items removes stock lines entirely. Select them with a filter on the query string — use this for a line that should not exist at all, rather than to set a quantity to zero.
| Name | Type | Required | Description |
|---|---|---|---|
filter[id][eq] | string | Optional | Remove the single stock line with this ID. |
filter[id][in] | string | Optional | Remove several lines at once — a comma-separated list of IDs. |
A write to these endpoints bypasses the order and supply flows — nothing is matched against
waiting orders, and no announcement is settled. Reach for them only when there genuinely is no
operation behind the change. If goods arrived from a supplier, announce them (step
3D); if a customer returned them, restock the order items (step
3A). The change still lands in the audit trail as a REGISTER
or CORRECTION stock mutation either way.
Step 5 — Set the stock thresholds#
Stock preferences are the policy around a SKU, per warehouse and business: when to reorder, how much to keep, and whether the SKU may be sold when it runs out. They do not change the quantity — they change what the platform does with it.
Input: CreateItemPreferencesInput! — a list of ItemPreferenceInput.
| Name | Type | Required | Description |
|---|---|---|---|
sku | String! | Required | The SKU the preference applies to. |
warehouse_id | ID! | Required | The warehouse the preference applies to. |
business_id | ID! | Required | The business the preference applies to. |
minimum_quantity | Int64! | Required | Lowest stock level you want to hold. |
optimal_quantity | Int64! | Required | Stock level a replenishment run aims for. |
maximum_quantity | Int64! | Required | Highest stock level you want to hold. |
reorder_point | Int64! | Required | Level at which a reorder is triggered. |
is_backorder_allowed | Boolean! | Required | Whether the SKU can still be ordered once it is out of stock. |
is_restricted_to_single_position | Boolean! | Required | Whether this SKU may only be held at one position in the warehouse. |
preorder_deadline_at | Int64 | Optional | Unix timestamp (milliseconds) until which the SKU can be pre-ordered. |
id | ID | Optional | Optional ID to assign to the preference. |
Returns: CreateItemPreferencesPayload
To change an existing preference, use updateItemPreferences. Its input is the same, except it is keyed on the preference id and takes no sku or business_id — read the current preferences first with the stockPreferences query:
updateItemPreferences replaces the whole preference, so send every field — omitted numbers are
not left alone. Remove a preference entirely with removeItemPreferences, which takes a list of
preference IDs.
Step 6 — Verify with the stock mutation log#
Every change you made above lands in stockMutations. Use it to confirm a mutation was processed, to see which quantity it resulted in, and to check whether the new quantity was broadcast to each connected sales channel.
Arguments:
| Name | Type | Required | Description |
|---|---|---|---|
query | QueryFilter | Optional | A query string of filters and sorting, e.g. `sku=SHIRT-BLUE-M&sort=-created_at`. |
first | Int | Optional | Page size. |
after | String | Optional | Cursor to continue from. |
Each node is a StockMutation:
| Name | Type | Required | Description |
|---|---|---|---|
id | ID! | Optional | Identifier of the stock mutation. |
source | StockMutationSource! | Optional | What caused the change — see the table at the top of this guide. |
status | StockMutationStatus! | Optional | `PENDING`, `PROCESSED` or `FAILING`. |
new_quantity | Int64! | Optional | The quantity that resulted from this change. |
product | Product! | Optional | The product whose stock changed. |
location | Location! | Optional | The location the change happened at. |
order | Order | Optional | The order behind the change, when there is one. |
actor | StockMutationActor!▾ | Optional | Who or what triggered the change. |
references | [StockMutationReference!]! | Optional | The entities behind the change — an `Order`, `Supply`, `DeliveryRequest`, `Reservation`, `Announcement` or `InventoryList`. Select them with inline fragments. |
messages | [StockMutationMessage!]!▾ | Optional | One entry per sales channel the new quantity was broadcast to. |
A mutation with status: PENDING has been accepted but not applied yet — re-read inventory
after it flips to PROCESSED. A FAILING mutation did not land; check its messages to see
which channel rejected the broadcast.
Notes#
- Stock is held per business and warehouse. The same SKU can have completely different quantities and preferences in each combination, so always pass
warehouse_idandbusiness_idwhen you filter or write. Int64timestamps in these inputs are Unix timestamps in milliseconds. Divide by 1000 for a standard Unix timestamp.- Monetary amounts on
costare integers in cents. quantity_expectednever becomes sellable on its own — it only moves intoquantity_availableonce the supply is actually received in the warehouse.- The inventory endpoints in step 4 are the only part of this guide that is not GraphQL, and they take camelCase field names where the rest of the API uses snake_case.
- See Authentication for how to pass your API key, and Pagination for how the
first/aftercursors work.