Stock
Stock in Morffeus is a quantity per product variant per warehouse. Your ERP or warehouse system stays the source of truth. Send changes with PUT /int/stocks, or set a warehouse to the quantities you counted with POST /stock/reconcile. Reads give you the numbers back per stock row, or summed across warehouses and stores.
How rows are matched#
You never send our ids. Every row names the variant and the warehouse the way your system knows them, and we resolve them:
- Variant: by
codefirst, thenbarcode, thenexternal_ref. We use the first one that finds a variant, so send the identifier you are surest of. - Warehouse: by
warehouse_code, the code you gave the warehouse in Admin or throughPUT /warehouse.
Each row succeeds or fails on its own. A row that names a variant or a warehouse we cannot find, or that would take a quantity below its minimum, comes back in failed with the reason. The other rows are written. The response is 200 either way, so always read failed.
The stock object#
What PUT /int/stocks returns for every row it wrote. One object per variant per warehouse.
| Field | Description |
|---|---|
| idinteger | Our id of the stock row. |
| product_variant_idinteger | Our id of the variant. |
| product_idinteger | Our id of the product the variant belongs to. |
| warehouse_idinteger | Our id of the warehouse. The warehouse_code you sent is on the warehouse object. |
| quantitynumber | Quantity on hand in the variant's unit. Decimals are allowed. |
| min_qtynumber · default 0 | The lowest quantity this row can reach. A change that would take quantity below it fails, so with the default 0 stock cannot go negative. |
| track_stockboolean | true for rows created through this API. |
| app_idinteger | Your app. |
| updated_by_user_idinteger · nullable | The user behind the last change, when we know it. null after a change through PUT /int/stocks. |
| inserted_at, updated_atdatetime · ISO 8601, UTC | When the row was created and last changed. Sent without an offset, for example 2026-09-30T09:41:12. |
Update stock levels#
Changes the stock of one or many variants in one or many warehouses. A row for a variant that has no stock in that warehouse yet creates the stock row with the quantity you send. A row for an existing stock row adds the quantity you send to it. Rows you leave out are untouched.
The quantity is added, not set. Sending 5 for a row that holds 40 leaves 45, and sending the same batch twice counts it twice. To send the totals your system holds, use Reconcile a warehouse, or send the difference from what you sent last.
The stock reads, GET /stocks and GET /stocks/aggregate, show the change at once. Product listings, and the storefront built on them, read stock from a search cache that picks the change up on its next scheduled refresh. No stock movement is recorded; when you need the history, book stock movements instead.
Body parameters
| Parameter | Description |
|---|---|
| stocksrequiredarray of objects | One entry per variant per warehouse. |
| stocks[].codeone ofstring | Variant code, the same value as code on the product variant. |
| stocks[].barcodeone ofstring | A barcode stored on the variant. |
| stocks[].external_refone ofstring | Your own id for the variant, as sent with the product. |
| stocks[].warehouse_coderequiredstring | Code of the warehouse. A code we cannot find fails the row. |
| stocks[].quantityrequirednumber | For a new stock row, its quantity. For an existing row, the amount to add; send a negative number to take stock away. |
| stocks[].min_qtynumber · default 0 | Minimum for a new stock row. Ignored when the row exists. |
Returns
data.success: the stock objects written. data.failed: one entry per row that was not written, with the row you sent in input and the reason in error.message, for example product variant or warehouse not found. Neither list keeps the order you sent, so match failures by input.
Errors
| Status | When |
|---|---|
| 200 | Also when some or all rows failed. They are in data.failed. |
| 400 | The body has no stocks array. |
| 401 | Missing or revoked token. The body names the token field. |
curl -X PUT "https://api.morffeus.com/api/v2/int/stocks" \ -H "Authorization: Bearer $MORFFEUS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "stocks": [ { "code": "SKU-1001", "warehouse_code": "WH-NS", "quantity": 42, "min_qty": 5 }, { "barcode": "8600123456789", "warehouse_code": "WH-BG", "quantity": -3 }, { "external_ref": "ERP-778", "warehouse_code": "WH-NS", "quantity": 12.5 } ] }'
const res = await fetch('https://api.morffeus.com/api/v2/int/stocks', { method: 'PUT', headers: { Authorization: `Bearer ${process.env.MORFFEUS_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ stocks: [ { code: 'SKU-1001', warehouse_code: 'WH-NS', quantity: 42, min_qty: 5 }, { barcode: '8600123456789', warehouse_code: 'WH-BG', quantity: -3 }, { external_ref: 'ERP-778', warehouse_code: 'WH-NS', quantity: 12.5 }, ], }), }); const { data } = await res.json(); if (data.failed.length) console.warn(data.failed);
{
"data": {
"success": [
{
"id": 5102,
"product_variant_id": 91388,
"product_id": 48377,
"warehouse_id": 17,
"quantity": 20.5,
"min_qty": 0.0,
"track_stock": true,
"app_id": 12,
"updated_by_user_id": null,
"inserted_at": "2026-08-14T07:02:55",
"updated_at": "2026-09-30T09:41:12"
},
{ "id": 5101, "product_variant_id": 91120, "quantity": 42.0, "min_qty": 5.0, … }
],
"failed": [
{
"input": { "barcode": "8600123456789", "warehouse_code": "WH-BG", "quantity": -3 },
"error": { "message": "product variant or warehouse not found" }
}
]
}
}
{
"errors": {
"token": "API.Autorization.Unauthorized"
}
}
List stock#
Stock rows of the app, one per variant per warehouse, with the product's detail fields attached. Use it to check a sync, not to poll: read stock movements when you need what changed since a point in time.
Query parameters
| Parameter | Description |
|---|---|
| warehouseIdinteger | Only rows of this warehouse. A warehouse of another app gives 400. |
| productIdinteger | Only rows of this product. |
| productVariantIdinteger | Only rows of this variant. |
| showPerWarehouseflag | Adds warehouse_code to every row. Any value turns it on, false included, so leave it out when you do not want it. |
| offsetinteger · default 0 | Rows to skip and rows to return. See pagination. |
| limitinteger · default 30 |
Returns
data: a list of rows, ordered by product, each with id (the stock row), product_id, quantity, min_qty, measuring_unit_short (the unit's short name in your token's language), warehouse_code when you ask for it, and the product's detail fields as your app defines them. There is no page count or total: ask for the next page until you get fewer rows than limit.
curl "https://api.morffeus.com/api/v2/stocks?warehouseId=17&showPerWarehouse=true&limit=2" \ -H "Authorization: Bearer $MORFFEUS_TOKEN"
{
"data": [
{ "id": 5101, "product_id": 48213, "warehouse_code": "WH-NS", "quantity": 42.0, "min_qty": 5.0, "measuring_unit_short": "pcs", "name": "Espresso beans 1 kg" },
{ "id": 5102, "product_id": 48377, "warehouse_code": "WH-NS", "quantity": 20.5, "min_qty": 0.0, "measuring_unit_short": "kg", "name": "House blend, loose" }
]
}
Totals across warehouses#
On-hand, reserved and available quantities for one variant, or for every active variant of a product, summed over the app's warehouses and split per variant and per store. Reserved is what checkouts in progress and stock write-offs hold. Available is on hand minus reserved, never below zero. This is the number to show when a product is not tied to a single store.
Query parameters
| Parameter | Description |
|---|---|
| productVariantIdone ofinteger | One variant. Wins when you send both. |
| productIdone ofinteger | Every active variant of this product. |
Returns
data.kpis: the totals, with the number of warehouses and stores that have stock rows for it. data.by_variant: the totals per variant, keyed by our variant id. data.app_locations: the totals per store, each with its warehouses. Without either parameter the call fails with 400.
{
"data": {
"kpis": { "on_hand": 62.5, "reserved": 2.0, "available": 60.5, "warehouses": 2, "locations": 2 },
"by_variant": {
"91120": { "on_hand": 62.5, "reserved": 2.0, "available": 60.5 }
},
"app_locations": [
{
"id": 31, "name": "Novi Sad centre",
"on_hand": 42.0, "reserved": 2.0, "available": 40.0,
"warehouses": [ { "id": 17, "name": "Novi Sad", "on_hand": 42.0, "reserved": 2.0, "available": 40.0 } ]
},
{ "id": 32, "name": "Belgrade west", … }
]
}
}
{
"errors": {
"stocks": ["Stocks.ProductVariantIdRequired"]
}
}
Reconcile a warehouse#
Sets the variants you list in one warehouse to the quantities you counted. We compare each with what the warehouse holds and book the differences as one stock movement, so the change keeps a history and product listings update straight away. Use it after a stock count, or whenever your system sends totals rather than changes.
Variants you leave out keep their quantity. Reconcile only sets what you list. It matches variants by their code only, sent as sku. A known code with no stock in the warehouse gets a stock row first, starting at zero.
Body parameters
| Parameter | Description |
|---|---|
| warehouse_coderequiredstring | The warehouse to reconcile. |
| productsrequiredarray of objects | One entry per variant. |
| products[].skurequiredstring | Variant code. |
| products[].quantityrequirednumber | The quantity you counted. |
| stock_movement_numberstring | Your reference for the stock movement. We make up a 12-character one if you leave it out. |
| descriptionstring · default "Product stock adjustment" | Description of the stock movement. |
Returns
data.failed: the entries we could not apply, each with the entry you sent in product and the reason in error, for example Could not find any product or variant with SKU: SKU-9999. Entries whose quantity already matches are skipped. An empty list means every entry was applied.
Errors
| Status | When |
|---|---|
| 404 | No warehouse with this warehouse_code in your app. |
| 422 | warehouse_code is not a string, or products is not a list. |
curl -X POST "https://api.morffeus.com/api/v2/stock/reconcile" \ -H "Authorization: Bearer $MORFFEUS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "warehouse_code": "WH-NS", "stock_movement_number": "COUNT-2026-09", "products": [ { "sku": "SKU-1001", "quantity": 40 }, { "sku": "SKU-1002", "quantity": 7 }, { "sku": "SKU-9999", "quantity": 3 } ] }'
{
"data": {
"failed": [
{
"product": { "sku": "SKU-9999", "quantity": 3 },
"error": "Could not find any product or variant with SKU: SKU-9999"
}
]
}
}
Related#
- Stock movements: changes with a type and a history. Reconcile books one for you.
- Warehouses: create warehouses and link them to stores with
PUT /warehouse. - Guide: Keep stock in sync: changes against counted totals, one warehouse or many, and what the storefront shows.