Stock and Warehouse API
With the Stock API your own system reads and changes your QrX warehouse: stock levels, items, locations, movements, receipts, purchase orders, pick lists, counts, containers and team tasks. This article is for developers. The base URL is https://api.qr-x.net and every call needs Authorization: Bearer <accessToken>; see "Developer Quickstart: QrX Public API and Webhooks" for the token. The full reference is at qrx.eu/api. No price, cost or stock value is ever returned.
Access and Scopes
Stock levels, items, locations, movements, counts and containers need
stock.read; creating or changing items and locations, and booking a correction, needstock.write. Receipts needinbound.readandinbound.write. Purchase orders needpurchasing.readandpurchasing.write. Pick lists needfulfilment.readand team tasks needtasks.read.A restricted key never gets the new parts by itself. Tick Purchasing, Fulfilment or Tasks under API access when the key needs them. Receipt lines only show
unitPriceandcurrencywhen the key may also read purchase orders.An API key that is not restricted has full access, so it can use the Stock API right away.
To limit a key, open Settings > Business > Merchant Channels, open the sales channel, tab Settings, block API access, and tick Read stock levels, items, locations, movements, counts and containers and Adjust stock and create or change items and locations as needed. See "Restrict an API Key (API Access)".
Your account needs QrX warehouse stock switched on. Without it the API answers 403
ACCESS_DENIED.
GET /api/v1/stock/levels
One row per stock item and warehouse. Query: stockItemId, sku, warehouseId, locationId, updatedSince (ISO 8601), cursor, limit (1 to 200, default 50) and scope.
{
"items": [{
"stockItemId": 345, "sku": "TSHIRT-M", "ean": "8712345678906",
"ownerCustomerId": null, "partnerCustomerId": null,
"warehouseId": 1, "warehouseCode": "MAIN",
"onHand": 42, "sellable": 40, "blocked": 2, "reserved": 5,
"incoming": 0, "backorder": 0, "available": 35,
"locationQuantity": null,
"lastSequence": 98231, "updatedAt": "2026-10-11T09:12:03Z"
}],
"nextCursor": "1-345"
}
reservedis everything open orders hold.availableissellableminusreserved.lastSequenceis the number of the last stock movement of that item in that warehouse. It is the same number assequencein thestock/adjustedwebhook.skumust match exactly; upper and lower case and outer spaces are ignored.With
locationIdyou only get items that have stock at that location, andlocationQuantityholds the units there.
GET /api/v1/stock/items
Your stock items, newest first. Query: sku, ean, updatedSince, cursor, limit (1 to 200, default 50) and scope.
{
"items": [{
"id": 345, "sku": "TSHIRT-M", "name": "T-shirt M", "ean": "8712345678906",
"unit": "piece", "kind": "simple", "trackingMode": "none", "isActive": true,
"ownerCustomerId": null, "partnerCustomerId": null, "weightG": 180,
"updatedAt": "2026-10-01T08:00:00Z"
}],
"nextCursor": 344
}
GET /api/v1/stock/locations
The locations that can hold stock, newest first. Query: warehouseId, cursor, limit (1 to 200, default 50).
{
"items": [{ "id": 77, "warehouseId": 1, "warehouseCode": "MAIN", "code": "A-01-02",
"kind": "bin", "zone": "A", "isActive": true }],
"nextCursor": 76
}
POST /api/v1/stock/adjust
Books one stock correction. The header Idempotency-Key is required.
POST /api/v1/stock/adjust
Idempotency-Key: erp-count-2026-10-11-0001{ "stockItemId": 345, "sku": null, "warehouseId": null, "locationId": 77, "delta": -3,
"reasonCode": "correction", "note": "Cycle count by ERP", "lotCode": null, "expiryDate": null }
Give
stockItemIdorsku. Exactly one item must match.Give
locationId. An account without bins may givewarehouseIdinstead, or nothing for the default warehouse.deltais the change in units: positive adds, negative removes. It cannot be 0.reasonCodeis required:correction,found,damaged,expired,defective,lost,theft,sampleorother.For an item with lots, give
lotCodeand, when the item needs one,expiryDate.
The answer holds the new level of the item in that warehouse:
{ "movementId": 98231, "stockItemId": 345, "warehouseId": 1, "locationId": 77, "delta": -3,
"onHand": 42, "sellable": 40, "available": 35, "sequence": 98231, "replayed": false }
Warehouse Resources
All lists use cursor and limit (1 to 100, default 50) and answer items and nextCursor, newest first. These resources are your own warehouse data only: scope=partner answers 422. The full request and answer fields are in the reference at qrx.eu/api.
Receipts:
GET /api/v1/receipts(filterstate,warehouseId,q),GET /api/v1/receipts/{id},POST /api/v1/receipts(withannounce: truethe receipt is announced),PATCH /api/v1/receipts/{id},POST .../{id}/lines,POST .../{id}/completeandPOST .../{id}/cancel.Purchase orders:
GET /api/v1/purchase-orders,GET /api/v1/purchase-orders/{id},POST /api/v1/purchase-orders(a draft),PATCH .../{id},POST .../{id}/lines,POST .../{id}/send,POST .../{id}/closeandPOST .../{id}/cancel. Send works exactly like in PortalX: withemail: trueQrX e-mails the PDF to the supplier (or toto), at most 5 times per order and 30 times per account per hour (429RATE_LIMIT_EXCEEDED).Stock items and locations:
POSTandPATCH /api/v1/stock/itemsand/api/v1/stock/locations. Locations need Full WMS with bins. The rows ofGET itemsandGET locationsnow haveversion.Reads:
GET /api/v1/stock/movements,GET /api/v1/picklists,GET /api/v1/counts(with/{id}and/{id}/lines),GET /api/v1/containersandGET /api/v1/tasks(with/{id}). Team tasks are read only.A change of an existing record needs the
versionyou read last. When someone changed the record in the meantime, you get 409: read it again and retry.Warehouses (scope
stock.read, Full WMS):GET /api/v1/warehouseswith code, name, default, time zone, address, contact, receiving instructions, loading docks and GLN;GET /api/v1/warehouses/{id},.../{id}/hours(receiving and dispatch hours per weekday) and.../{id}/closures(withfromandcursor).Transfers (scope
stock.read, Full WMS with multiple warehouses and transfers):GET /api/v1/transfers(filterstate,warehouseId,direction) andGET /api/v1/transfers/{id}with the lines and theshippingdetails. Read only.Purchase orders: create and
PATCHaccept the header fieldsreference,supplierReference,buyerUserId,internalNote,incoterm,incotermPlace,paymentTermsDays,shippingCost,shippingVatRate,pricesIncludeVat,deliveryAddressandpartialDeliveryAllowed, and lines acceptdiscountPct,vatRate,note,confirmedQtyandconfirmedOn. Orders return the totals (net, VAT per rate, gross) calculated by QrX. The list filters onreference,expectedFromandexpectedTo. Terms are fixed once the order is sent.Receipts: create and
PATCHacceptsupplierIdto link a receipt to a supplier; rows return it.Stock items and locations: location writes accept
zoneorzoneIdand the size and capacity fields; item writes acceptstorage,dangerousGoods,imageUrlandabcClassOverride.POST /api/v1/stock/adjustaccepts your own reason codes.
Idempotency-Key
Send your own unique text per correction. When you retry the same request with the same key, you get the first answer again with
replayed: trueand nothing is booked twice.The same key with a different body is refused with 409
DUPLICATE_IDEMPOTENCY_KEY.A key cannot start with a QrX system prefix such as
mcp-and cannot end in a colon followed by digits (422).Every POST and PATCH of the warehouse resources needs the header
Idempotency-Key(1 to 140 characters). The same key with the same body on the same route gives the first answer again, with the headerIdempotent-Replayed: true. A replayed create of a receipt or purchase order answers the record as it is now.Answers are kept for 30 days. A call that failed (4xx or 5xx) is not kept, so you may send the same key again.
While the first call with a key is still running you get 409
CONCURRENCY_CONFLICTwithRetry-After. A call that stopped halfway frees its key after 15 minutes.Sending a purchase order with
email: truee-mails the supplier at most once per key. If a call fails after the order was sent but before the e-mail, no e-mail follows for that key: check the order's e-mail history in PortalX and send again with a new key.
Cursors and updatedSince
Every list returns
itemsandnextCursor. SendnextCursorback ascursorfor the next page. Null means the end. There is no total count.Treat a cursor as an opaque value.
With
updatedSinceyou only get rows changed at or after that time. The server looks at up to 1,000 rows per call, so a page can hold fewer rows thanlimit, even none, whilenextCursoris set. Keep calling untilnextCursoris null.
Stock at a Fulfilment Partner: scope=partner
When a fulfilment partner holds your stock, add scope=partner to GET levels and GET items to read your own stock at your partners. Without it you get the default, scope=warehouse: the stock in your own warehouses.
Each row has
partnerCustomerId(the partner warehouse) and your own id inownerCustomerId.Use
partnerCustomerIdin the query to keep one partner, andwarehouseIdfor one of its warehouses.Locations are never returned:
locationId,GET locationsandPOST adjustanswer 422 in this scope.You need at least one partner with active access; your own account does not need warehouse stock switched on.
Error Codes
401
UNAUTHORIZED: no valid access token.403
STOCK_SCOPE_MISSING: the key is restricted and lacksstock.readorstock.write.403
ACCESS_DENIED: warehouse stock is not on for the account, or, withscope=partner, no partner has active access.404
STOCK_NOT_FOUND: the item, warehouse or location is not yours.400
IDEMPOTENCY_KEY_REQUIRED: a POST without the header.422
VALIDATION_FAILED: bad input, for example delta 0, an unknown reason code, a bad cursor, or more than 50 items with the same SKU or EAN (filter bystockItemId).409
DUPLICATE_IDEMPOTENCY_KEY: the same key with a different body.409
STOCK_CONFLICT: not enough stock, stock that is allocated to a pick list, a SKU that matches several items, or an inactive item.409
CONCURRENCY_CONFLICT: send the same request again with the same key.429: too many requests; wait for
Retry-After.403
INBOUND_SCOPE_MISSING,PURCHASING_SCOPE_MISSING,FULFILMENT_SCOPE_MISSINGorTASKS_SCOPE_MISSING: the key is restricted and lacks that part.404
INBOUND_NOT_FOUND,PURCHASE_ORDER_NOT_FOUNDorNOT_FOUND: the record is not yours.409
INBOUND_CONFLICTorPURCHASING_CONFLICT: the version or the state does not allow the change.429
RATE_LIMIT_EXCEEDED: too many supplier e-mails for a purchase order.400
ZONE_UNKNOWN: the zone is not a live zone of that warehouse. 400DG_CLASS_UNKNOWN: the dangerous goods class is not a visible class of your account. A reason that needs photos cannot be used through the API.
When to Contact Our Support Team
Contact QrX support when the API answers ACCESS_DENIED although warehouse stock is on, or when a level differs from PortalX. Include the requestId of the answer, the SKU and the time. Never send the API key or the token.
