Who Is This Document For?
This guide explains how your shop, such as Shopify, Wix, eBay, or a custom website, can automatically send “please ship this order” requests to our warehouse system when an order is placed. It also explains how your ERP can check real-time stock in our warehouse.
If “API” sounds technical, think of it as a fixed order form between systems. When you send information in the agreed format, the receiving system understands it and processes the work automatically.
Before you start
This document is for the customer's developer.
- A safe place for the API key — e.g. an environment variable on your server. Never put it in browser or app code.
- For webhooks, a publicly reachable HTTPS address. Verify each signal's signature (whsec_…) and answer with a 2xx quickly.
No developer? Ask your warehouse about CSV bulk upload, AI order entry, or entering orders on your customer screen — the recommended path. You can also set up a no-code automation tool (Zapier, Make, n8n…) yourself to receive webhooks and call the API, or have EZwarehouse build the customer-side integration or set up such a tool as a separately quoted service, through your warehouse.
Overall Flow
- Prepare: API connections are set up by EZwarehouse at your warehouse's request. Once the setup is agreed, EZwarehouse issues your API keys — a test key (
sk_test_) and a live key (sk_live_). Treat them like passwords. - Order placed: A customer places an order in your shop.
- Send: Your shop sends the order details to our API automatically.
- Outbound created: Our system creates an outbound request.
- Completion notification: When shipping is completed, we can send a “shipped” signal back to your system.
Glossary
- API key (sk_live_...) — the secret password used to access our system.Never share it publicly. If it is exposed, ask the warehouse for a new key immediately — the old key stops working the moment a new one is created.
- Endpoint — the URL that receives a request, such as
/api/outbound/create. - Request / Response — the message you send / the answer we return.
- SKU — the product code registered in our system.
- externalOrderId — your shop order number. Sending the same value twice prevents duplicate outbound creation. We recommend always sending it.
- Webhook — an automatic event notification:
- Your shop → us, for order-created events
- Us → your shop, for outbound-shipped and inbound-completed events
Getting Started 1: Custom Shop
If you run a custom-built shop, send one request to us whenever an order is placed.
1. Get an API key — only once. Your warehouse asks EZwarehouse to set up the connection; once it is agreed, EZwarehouse hands a test key (sk_test_…) and a live key (sk_live_…) to your developer directly. A key is shown only once, so store it safely right away. A revoked key, or one replaced by a new key, stops working immediately (401). If a key is exposed, tell your warehouse at once — they can stop it straight away, and EZwarehouse issues the new one.
2. Send the order request:
POST https://ezwarehouse.com.au/api/outbound/create
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
{
"warehouseName": "Main Warehouse",
"tradeType": "Normal",
"orderType": "normal",
"shipTo": "Jane Smith",
"shipAddress": "123 Example Street",
"shipContact": "+61 400 000 000",
"externalOrderId": "ORD-2026-0001",
"products": [
{ "sku": "ITEM-A", "quantity": 2 },
{ "sku": "ITEM-B", "quantity": 1 }
]
}Fields:
- warehouseName (required) — the warehouse to ship from, exactly as it is named in our system.
- tradeType (required) — the kind of shipment. Use
"Normal"for an ordinary sale."STO"moves stock to another warehouse (also needsdestinationWarehouseName);"SCO"is a stock-correction shipment. Case-insensitive. - products (required) — 1 to 200 lines, each with
sku(exactly your product code in our system) andquantity(a whole number of 1 or more). Serial-tracked products may also send aserialNumbersarray. - externalOrderId (optional, strongly recommended) — your order number. If a network error makes you resend, no second outbound is created.
- shipTo / shipAddress / shipContact / instruction (optional) — recipient / address / phone / handling note.
- requiredShipDate (optional) —
dd/MM/yyyyoryyyy-MM-dd; defaults to today. - orderType (optional) — defaults to
"normal".
Example response:
{ "success": true, "reference": "OUT20260102001", "status": "pending" }Resending the same externalOrderId creates nothing new and returns the original reference. The status in this response is always pending, so check the real progress with “Check Outbound Status” below.
{ "success": true, "duplicate": true, "reference": "OUT20260102001", "status": "pending" }No order number of your own? Use the Idempotency-Key header. Generate a fresh unique value (a UUID, say) for each request and send it as Idempotency-Key. If you get no response and send again, send the same key with the same body: the original reference comes back with "duplicate": true and only one outbound is ever created. Reusing a key for a request with different contents is refused with 422 — use a new key for a new order. With neither the header nor externalOrderId, every call creates a new outbound, as before.
Idempotency-Key: 3f1e6c2a-9b0d-4a51-8f7e-2c4d5b6a7e81 requiredShipDate is in the past, the request is treated as already shipped, current stock is checked, and it is rejected with 409 if short.Getting Started 2: Shopify / Wix / eBay / Amazon
For Shopify, install the EZwarehouse app. As well as receiving orders, it reports shipments and tracking numbers back to the store, applies the store's order cancellations to outbound requests, and can add warehouse receipts to the store's stock. No webhook URL or API secret changes hands.
- Client Management → customer row → Platform Integrations → Shopify tab.
- Enter the store's myshopify.com address as the Shop Domain and click Connect on Shopify.
- Approve the install on Shopify's screen. Only someone signed in as the store owner sees it, so the customer often has to click through it themselves.
- Back in Platform Integrations, choose the default warehouse and the Shopify location, turn on the features you want (receive orders / report shipments and tracking / apply cancellations / add received stock), and save.
Manual connection (legacy Shopify, Wix / eBay / Amazon): the platform posts orders to our webhook URL. Orders come in; nothing is sent back to the store.
- Go to Client Management → customer row → Platform Integrations.
- Enter the platform identifier, such as Shopify domain (legacy route only — not needed once the app is connected), Wix Instance ID, eBay username, or Amazon Seller ID.
- Enter the platform secret if available. This helps block fake requests.
- Select the default warehouse for incoming orders and enable the integration.
- Copy the displayed Webhook URL into the platform admin screen.
Getting Started 3: Check Inventory From Your ERP
Your ERP or internal system can fetch current stock in real time using the same API key. Customers can only see their own stock.
GET https://ezwarehouse.com.au/api/inventory
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
# Optional filters
# ?sku=ITEM-A
# ?warehouse=Main Warehouse
# ?limit=200Example response:
{
"success": true,
"items": [
{
"sku": "ITEM-A",
"productName": "Product A",
"unit": "EA",
"totalQuantity": 120,
"lotCount": 3,
"lots": [
{ "location": "A-01-01", "quantity": 100, "expiryDate": null, "serialNumber": null }
]
}
],
"truncated": false
}- totalQuantity — current total stock for the SKU.
- lots — full detail by location, expiry date, and serial number.
- truncated: true — the result was partially summarized. Narrow the request with
skuorwarehouse.
Check Outbound Status
You can check progress anytime using the reference we return.
GET https://ezwarehouse.com.au/api/outbound/OUT20260102001
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxThe response includes the current status plus the product list. Once a picking plan exists, pickingPlan also carries location info (original rack cell → current location). Once the warehouse enters them, carrierName and trackingNumber carry the carrier and tracking number (null until then). The tracking number is often entered after the order has shipped, so subscribe to the outbound.tracking_updated notification below or read again. The list (GET /api/outbound) carries the same two fields. If there is no such outbound, you get 404.
What status means — outbounds created through the API start at pending.
pending— waiting (received, work not started)processing— in progress (being picked)ready— ready (picked, waiting to ship)shipped— shippedcancelled— cancelledrequested— awaiting approval (a request the warehouse must approve)changes_requested/rejected/withdrawn— changes requested / rejected / withdrawn (only for requests in the approval step)
To list several at once, use GET /api/outbound?status=shipped&limit=20 (newest first, limit 1–100, default 20). Inbounds work the same way with GET /api/inbound and GET /api/inbound/INBOUND_REFERENCE. Inbounds cannot be created through the API.
Receive Shipping and Receiving Completion Notifications
When we finish shipping (outbound.shipped) or complete a receipt (inbound.completed), we automatically send a signal to your system. You can use it to update order status in your shop. outbound.shipped carries the carrier and tracking number entered so far (carrierName, trackingNumber); one entered or changed after shipping arrives as its own outbound.tracking_updated signal.
- Tell your warehouse the URL that should receive signals (a publicly reachable HTTPS address) and which notices you want.
- EZwarehouse registers it; the signing secret (
whsec_…) is shown once and EZwarehouse passes it to you. Store it in your backend — you use it to verify incoming signals. - When EZwarehouse sends a test, one
pingevent arrives (a connectivity check, never retried).
Example notification (outbound.shipped):
{
"event": "outbound.shipped",
"timestamp": "2026-01-02T15:30:00.000Z",
"data": {
"reference": "OUT20260102001",
"externalOrderId": "ORD-2026-0001",
"status": "shipped",
"shippedDate": "02/01/2026",
"carrierName": "Australia Post",
"trackingNumber": "33ABC0012345",
"products": [
{ "sku": "ITEM-A", "productName": "Product A", "quantity": 2, "unit": "EA", "serialNumbers": [] }
]
}
}For inbound.completed, data carries reference, containerNumber, status, completionDate, and products (sku, productName, quantity, location, serialNumbers).
outbound.tracking_updated arrives when a carrier or tracking number is entered or changed on an outbound, including after it shipped. Its data carries reference, externalOrderId, status, shippedDate, carrierName, and trackingNumber. It is sent whenever the value differs from the last one sent — correcting A → B → A sends three, saving the same value again sends none. Clearing a number that was sent sends one notice with carrierName and trackingNumber both null (tracking removed).
Choosing events: the events are chosen when EZwarehouse registers the webhook. A webhook set up from now on receives every event; one set up before event selection existed keeps receiving only outbound.shipped and inbound.completed, as before. To receive outbound.tracking_updated, ask through your warehouse. Your warehouse can only switch a webhook off.
Headers sent with each signal:
X-Webhook-Signature: sha256=…— the signature (see the verification example below)X-Webhook-Event— the event nameX-Webhook-Delivery— the delivery ID. It is the same on every retry, so use it to skip duplicates.X-Webhook-Attempt— which attempt this is (starting at 1)
Verifying the webhook signature (Node.js example):
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // the full whsec_… string you were given
// Compute the signature over the raw body exactly as received. Parsing JSON and re-serialising it changes the bytes.
app.post('/webhooks/warehouse', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Webhook-Signature') || '';
const received = Buffer.from(header.startsWith('sha256=') ? header.slice(7) : '', 'hex');
const expected = Buffer.from(crypto.createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex'), 'hex');
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.status(401).send('invalid signature');
}
const deliveryId = req.get('X-Webhook-Delivery'); // already processed? return 200 and skip
const { event, data } = JSON.parse(req.body.toString('utf8'));
// event: 'outbound.shipped' | 'outbound.tracking_updated' | 'inbound.completed' | 'ping'
res.status(200).send('ok');
});X-Webhook-Delivery values you have processed and skip repeats.Keeping calls down — expected monthly calls by sync interval
The connection fee follows your monthly API calls (orders and lookups together) and is arranged through your warehouse. The call count is mostly set by how often you sync. Calls are never blocked for volume; only the per-minute limits apply.
| Pattern | Expected calls a month |
|---|---|
| Inventory sync every 15 min | ≈ 3,000 |
| Inventory sync every 5 min | ≈ 9,000 |
| Inventory sync every minute | ≈ 45,000 |
| One outbound (create + status checks) | ≈ 5 |
Plans and Rate Limits
API keys are issued per customer, and every endpoint called with one requires that customer's connection to be connecting, testing or active. API connections are set up by EZwarehouse at your warehouse's request; fees and terms are arranged through your warehouse. Without an active connection, every endpoint returns 402 Payment Required.
Limits depend on the tier (set for each customer together with EZwarehouse as part of that customer's integration service) and are counted per customer in fixed one-minute windows (seconds 0–59 of each clock minute).
| Tier | Create outbound / min | Read / min |
|---|---|---|
| Tier 1 | 60 | 120 |
| Tier 2 | 200 | 400 |
| Tier 3 (Enterprise default) | 600 | 1,200 |
Creates and reads share one per-minute counter. For example, on Tier 1, once you have made more than 60 calls in that minute (reads included), outbound creation returns 429. Authenticated responses carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (reset time, Unix seconds), and X-RateLimit-Tier.
Common Response Codes
Errors look like { "success": false, "error": "…" }; error explains the reason.
- 400 Bad Request — the request is invalid: a required field (
warehouseName,tradeType,products) is missing, the warehouse name does not match (“Warehouse '…' not found”), a SKU is unregistered or inactive, or a quantity is not a whole number of 1 or more. - 401 Unauthorized — the
Authorization: Bearer KEYheader is missing or the key is wrong (“Invalid API Key”). A revoked key, or an old key replaced by a newly generated one, also returns 401. Ask for the current key through your warehouse; EZwarehouse issues it. - 402 Payment Required — your connection is not switched on (API access is switched on per customer). Contact your warehouse.
- 403 Forbidden — this key is not allowed to do that. Outbound creation requires a customer API key (
sk_live_). - 404 Not Found — no outbound/inbound with that reference (or it does not belong to you).
- 409 Conflict —
requiredShipDateis in the past and stock is short, the customer account is being deleted, or the order with thatexternalOrderIdwas already cancelled on the store. - 422 Unprocessable Entity — an
Idempotency-Keywas reused for a request with different contents. Use a new key for a new order. - 429 Too Many Requests — the per-minute limit was exceeded. Wait for the
Retry-Afterheader (seconds), then retry. - 500 Internal Server Error — the body is not valid JSON, or a temporary server error. Check the JSON and retry shortly (with
externalOrderIdor anIdempotency-Keyset, a retry never creates a duplicate).
Sandbox (test environment)
1. What it is, and why
The sandbox is a separate copy of your account filled with fake data, where you can build and test your integration end to end before touching real stock. Nothing you do there reaches the warehouse floor: no one picks, packs or receives anything, and no order is ever shipped.
It uses the same base URL and the same endpoints as production. The only thing that decides whether you are talking to the sandbox or to your real account is the API key you send. On top of the normal endpoints, the sandbox has two simulation endpoints that make the warehouse "do" its part on request, so you can see your orders ship and your receipts complete — with the same webhooks production sends.
2. How to get access
- Ask your warehouse for API sandbox access. They request it from EZwarehouse, who set up a test customer for you — named after you with
(TEST)on the end — with copies of your products and some test stock. - EZwarehouse gives your developer the test key through a secure channel (never in a plain email or chat), once the setup is agreed through your warehouse.
- Use it exactly like a live key:
Authorization: Bearer sk_test_….
Telling the keys apart: look at the prefix.
sk_test_+ 32 characters — a test key. It opens only the sandbox, and is refused with 403 anywhere else.sk_live_+ 32 characters — a live key for your real account and real stock.
3. What data is in the sandbox
- Your products, copied from your live account: SKU, name, unit, barcode, unit conversions, category, and serial/date settings. Prices are never copied. A product added to your live account later is not in the sandbox until it is copied again — ask the warehouse.
- Starting test stock for every SKU you hold in each warehouse (100 units each unless agreed otherwise). It is put on the shelf through a normal receipt, so it shows up in history like real stock. Serial-tracked products get their test stock on request.
- Warehouses with exactly the same names as in production, so the
warehouseNamevalues (andtradeTypevalues) your integration sends in testing work unchanged when you go live.
4. Step-by-step walkthrough
Step 1 — Check stock.
curl "https://ezwarehouse.com.au/api/inventory?limit=20" \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"success": true,
"items": [
{
"sku": "ITEM-A",
"productName": "Product A",
"unit": "EA",
"totalQuantity": 100,
"lotCount": 1,
"lots": [
{ "location": "RECEIVE-CELL-1", "quantity": 100, "serialNumber": null,
"expiryDate": null, "warehouseId": "…", "receivedDate": "22/10/2026" }
],
"lotsTruncated": false
}
],
"truncated": false
}Step 2 — Create an outbound, exactly as in production (see “Getting Started 1” for every field).
curl -X POST "https://ezwarehouse.com.au/api/outbound/create" \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f1e6c2a-9b0d-4a51-8f7e-2c4d5b6a7e81" \
-d '{
"warehouseName": "Main Warehouse",
"tradeType": "Normal",
"shipTo": "Test Buyer",
"shipAddress": "1 Test Street",
"externalOrderId": "TEST-0001",
"products": [ { "sku": "ITEM-A", "quantity": 2 } ]
}'{ "success": true, "reference": "OUT20261022001", "status": "pending" }Step 3 — Simulate the shipment. No body is needed.
POST /api/sandbox/outbound/{reference}/shipcurl -X POST "https://ezwarehouse.com.au/api/sandbox/outbound/OUT20261022001/ship" \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"success": true,
"simulated": true,
"reference": "OUT20261022001",
"status": "shipped",
"shippedDate": "22/10/2026",
"carrierName": "SANDBOX COURIER",
"trackingNumber": "SBXOUT20261022001",
"webhook": {
"customer": { "dispatched": true, "deliveryId": "…", "status": "delivered", "httpStatus": 200, "error": null },
"warehouse": { "dispatched": false, "reason": "webhook-disabled" }
},
"trackingWebhook": {
"customer": { "dispatched": true, "deliveryId": "…", "status": "delivered", "httpStatus": 200, "error": null },
"warehouse": { "dispatched": false, "reason": "webhook-disabled" }
}
}The server does what the floor would: it picks from your available test stock by the same rules the warehouse uses (earliest lot first), stages the goods on the warehouse's loading dock and ships them — through the same code as a real shipment, so stock goes down, history is written and the outbound.shipped webhook is sent. It works on an outbound that is requested, pending, processing or ready; in the sandbox there is no one to approve a requested one, so the simulation approves it. webhook in the response is the result of the first delivery attempt to each subscriber (reason: "webhook-disabled" means no webhook is registered). Calling it again for a shipped outbound changes nothing and answers:
{ "success": true, "alreadyShipped": true, "reference": "OUT20261022001", "status": "shipped",
"shippedDate": "22/10/2026", "carrierName": "SANDBOX COURIER", "trackingNumber": "SBXOUT20261022001" }Step 4 — Receive the outbound.shipped webhook. It is the same event, with the same payload, headers and signature, as in production:
{
"event": "outbound.shipped",
"timestamp": "2026-10-22T05:12:09.000Z",
"data": {
"reference": "OUT20261022001",
"externalOrderId": "TEST-0001",
"status": "shipped",
"shippedDate": "22/10/2026",
"carrierName": "SANDBOX COURIER",
"trackingNumber": "SBXOUT20261022001",
"products": [
{ "sku": "ITEM-A", "productName": "Product A", "quantity": 2, "unit": "EA", "serialNumbers": [] }
]
}
}The simulated shipment records the carrier SANDBOX COURIER and the tracking number SBX + the outbound reference on the order, in the same fields a real carrier entry fills (carrierName, trackingNumber). Wherever the API reports carrier and tracking — on the outbound, in outbound.shipped, and in the outbound.tracking_updated event — the sandbox reports these test values. They are recorded together with the shipment, so outbound.shipped already carries them, and a webhook with outbound.tracking_updated ticked (a newly registered one has every event) then receives that event exactly once, right after outbound.shipped — what a real order sends when its tracking number is entered. trackingWebhook in the response is its first delivery attempt (reason: "event-not-selected" means the event is not ticked; "unchanged" means it was already sent).
{
"event": "outbound.tracking_updated",
"timestamp": "2026-10-22T05:12:10.000Z",
"data": {
"reference": "OUT20261022001",
"externalOrderId": "TEST-0001",
"status": "shipped",
"shippedDate": "22/10/2026",
"carrierName": "SANDBOX COURIER",
"trackingNumber": "SBXOUT20261022001"
}
}Step 5 — Read the outbound back.
curl "https://ezwarehouse.com.au/api/outbound/OUT20261022001" \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"success": true,
"reference": "OUT20261022001",
"status": "shipped",
"shippedDate": "22/10/2026",
"tradeType": "Normal",
"externalOrderId": "TEST-0001",
"carrierName": "SANDBOX COURIER",
"trackingNumber": "SBXOUT20261022001",
"products": [ { "sku": "ITEM-A", "productName": "Product A", "quantity": 2, "unit": "EA", "serialNumbers": [] } ],
"pickingPlan": [ { "sku": "ITEM-A", "quantity": 2, "originalLocation": "RECEIVE-CELL-1", "location": "LOADING-DOCK-1", "picked": true } ],
…
}Receipts (inbound). Receipts cannot be created through the REST API — not in production and not here. In the sandbox you can either ask the warehouse to book a test receipt for you, or create a receipt request yourself with the create_inbound tool of our MCP server (/api/mcp, same test key). Then complete it:
POST /api/sandbox/inbound/{reference}/completecurl -X POST "https://ezwarehouse.com.au/api/sandbox/inbound/IN20261022001/complete" \
-H "Authorization: Bearer sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"success": true,
"simulated": true,
"reference": "IN20261022001",
"status": "completed",
"completionDate": "22/10/2026",
"location": "RECEIVE-CELL-1",
"webhook": {
"customer": { "dispatched": true, "deliveryId": "…", "status": "delivered", "httpStatus": 200, "error": null },
"warehouse": { "dispatched": false, "reason": "webhook-disabled" }
}
}Every line is put into the warehouse's first receive cell (location above), stock goes up, and inbound.completed is sent. For serial-tracked products, the serial numbers on the receipt line are used when they cover its quantity; otherwise made-up ones (SBX-IN…-1-1, …) are created. Read it back with GET /api/inbound/IN20261022001.
5. Webhooks in the sandbox
- Register a test URL: send a public HTTPS address for the sandbox through your warehouse; EZwarehouse registers it. A request-inspection service (a “request bin”, e.g. webhook.site) is the quickest way to see what arrives; a tunnel to your development machine works too. The sandbox has its own webhook settings and its own signing secret (
whsec_…), separate from production. - Signatures and verification are identical to production: the same
X-Webhook-Signature,X-Webhook-Event,X-Webhook-DeliveryandX-Webhook-Attemptheaders, and the same verification code (see “Receive Shipping and Receiving Completion Notifications”). - Retries and resending work as in production: a failed delivery is retried automatically on the same schedule, the warehouse can resend it from Webhook Delivery History, and “Send Test” sends a
ping. A simulation called again for something already shipped or completed does not send the event twice — create and simulate a new order to see another one.
6. Differences from production
- No real picking or receiving: the simulation endpoints stand in for the warehouse. They exist only in the sandbox and answer 403 to a live key.
- Simulated shipments carry carrier
SANDBOX COURIERand trackingSBX…. History rows written by a simulation end with “[Simulated in the API sandbox — no goods moved]”. - No billing: nothing in the sandbox is ever invoiced.
- Rate limits work exactly as in production (same counters and
X-RateLimit-*headers). A sandbox customer is on Tier 1 (60 per minute, reads included); your production tier may differ. - All data is fake. Ask the warehouse to reset the sandbox or top up test stock whenever you need to; nothing resets on its own.
7. Going-live checklist
- Swap the key: replace
sk_test_…with yoursk_live_…key. The URL does not change. - Remove every call to
/api/sandbox/…— in production the warehouse ships and receives, and those calls answer 403. - Have EZwarehouse register your production webhook URL (ask through your warehouse), and put the production signing secret in your verifier (it differs from the sandbox one).
- Confirm the
warehouseNameandtradeTypevalues you send, and that every SKU you send exists in your live account. - Keep the safety nets you tested:
externalOrderIdorIdempotency-Keyon every create, retries on 429/5xx, and skipping webhook deliveries whoseX-Webhook-Deliveryyou have already processed. - Send one real, low-risk order while the warehouse watches, and confirm it ships and the webhook arrives.
8. Troubleshooting
| Response | Likely cause | What to do |
|---|---|---|
| 401 | Missing header, a typo in the key, or a key that was revoked or re-issued. | Check Authorization: Bearer sk_test_…; ask the warehouse for the current key. |
| 403 | Forbidden: simulation endpoints exist only in the API sandbox… — a simulation called with a live key. Forbidden: test keys (sk_test_) only work in a sandbox account — a test key used outside the sandbox. | Use the test key for the sandbox and the live key for production. |
| 402 | API access is not switched on for the test customer. | Ask the warehouse. |
| 404 | No outbound/inbound with that reference belongs to you. | Use the reference returned when it was created. |
| 409 | Not enough test stock (the message names each SKU and quantity short), or the order/receipt is in a status that cannot be simulated (e.g. cancelled), or it changed while being simulated. | Ask for a stock top-up or lower the quantity; create a new order; or retry. |
| 400 | Product with SKU '…' not found for your account or Warehouse '…' not found. | The SKU is not in the sandbox (ask for it to be copied) or the warehouse name differs — names are case- and space-sensitive. |
HTTP/1.1 409 Conflict
{ "success": false, "error": "Not enough available stock to ship 'OUT20261022002': ITEM-A x500. Ask for the sandbox stock to be topped up, or lower the quantities." }HTTP/1.1 403 Forbidden
{ "success": false, "error": "Forbidden: simulation endpoints exist only in the API sandbox and accept only a test key (sk_test_). In production the warehouse ships and receives for real." }Next Steps
Use the Technical Docs tab for the full field list, response formats, and error cases. You can also download the OpenAPI (.yaml) file and import it into tools such as Postman or Insomnia.