Skip to main content

Stock and Warehouse API

H
Written by Hayko

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, need stock.write. Receipts need inbound.read and inbound.write. Purchase orders need purchasing.read and purchasing.write. Pick lists need fulfilment.read and team tasks need tasks.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 unitPrice and currency when 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"
}

  • reserved is everything open orders hold. available is sellable minus reserved.

  • lastSequence is the number of the last stock movement of that item in that warehouse. It is the same number as sequence in the stock/adjusted webhook.

  • sku must match exactly; upper and lower case and outer spaces are ignored.

  • With locationId you only get items that have stock at that location, and locationQuantity holds 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 stockItemId or sku. Exactly one item must match.

  • Give locationId. An account without bins may give warehouseId instead, or nothing for the default warehouse.

  • delta is the change in units: positive adds, negative removes. It cannot be 0.

  • reasonCode is required: correction, found, damaged, expired, defective, lost, theft, sample or other.

  • For an item with lots, give lotCode and, 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 (filter state, warehouseId, q), GET /api/v1/receipts/{id}, POST /api/v1/receipts (with announce: true the receipt is announced), PATCH /api/v1/receipts/{id}, POST .../{id}/lines, POST .../{id}/complete and POST .../{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}/close and POST .../{id}/cancel. Send works exactly like in PortalX: with email: true QrX e-mails the PDF to the supplier (or to to), at most 5 times per order and 30 times per account per hour (429 RATE_LIMIT_EXCEEDED).

  • Stock items and locations: POST and PATCH /api/v1/stock/items and /api/v1/stock/locations. Locations need Full WMS with bins. The rows of GET items and GET locations now have version.

  • Reads: GET /api/v1/stock/movements, GET /api/v1/picklists, GET /api/v1/counts (with /{id} and /{id}/lines), GET /api/v1/containers and GET /api/v1/tasks (with /{id}). Team tasks are read only.

  • A change of an existing record needs the version you 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/warehouses with 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 (with from and cursor).

  • Transfers (scope stock.read, Full WMS with multiple warehouses and transfers): GET /api/v1/transfers (filter state, warehouseId, direction) and GET /api/v1/transfers/{id} with the lines and the shipping details. Read only.

  • Purchase orders: create and PATCH accept the header fields reference, supplierReference, buyerUserId, internalNote, incoterm, incotermPlace, paymentTermsDays, shippingCost, shippingVatRate, pricesIncludeVat, deliveryAddress and partialDeliveryAllowed, and lines accept discountPct, vatRate, note, confirmedQty and confirmedOn. Orders return the totals (net, VAT per rate, gross) calculated by QrX. The list filters on reference, expectedFrom and expectedTo. Terms are fixed once the order is sent.

  • Receipts: create and PATCH accept supplierId to link a receipt to a supplier; rows return it.

  • Stock items and locations: location writes accept zone or zoneId and the size and capacity fields; item writes accept storage, dangerousGoods, imageUrl and abcClassOverride. POST /api/v1/stock/adjust accepts 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: true and 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 header Idempotent-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_CONFLICT with Retry-After. A call that stopped halfway frees its key after 15 minutes.

  • Sending a purchase order with email: true e-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 items and nextCursor. Send nextCursor back as cursor for the next page. Null means the end. There is no total count.

  • Treat a cursor as an opaque value.

  • With updatedSince you 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 than limit, even none, while nextCursor is set. Keep calling until nextCursor is 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 in ownerCustomerId.

  • Use partnerCustomerId in the query to keep one partner, and warehouseId for one of its warehouses.

  • Locations are never returned: locationId, GET locations and POST adjust answer 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 lacks stock.read or stock.write.

  • 403 ACCESS_DENIED: warehouse stock is not on for the account, or, with scope=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 by stockItemId).

  • 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_MISSING or TASKS_SCOPE_MISSING: the key is restricted and lacks that part.

  • 404 INBOUND_NOT_FOUND, PURCHASE_ORDER_NOT_FOUND or NOT_FOUND: the record is not yours.

  • 409 INBOUND_CONFLICT or PURCHASING_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. 400 DG_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.

Did this answer your question?