WMS Implementation Guide
This is the single source of truth for integrating with the 888 Wholesale Warehouse Management System. It covers authentication, the stock model, idempotent writes, replenishment workflows, ASN receiving, and signed webhooks, with copy-paste examples for each.
1. System Overview
The WMS REST API is versioned under /api/wms/v1. It powers the 888 WMS Desktop Client and any partner integration that needs to read or mutate stock held in a warehouse owned by an 888 Wholesale supervisor account.
Every read endpoint returns Cache-Control: no-store, max-age=0, must-revalidate. Your client and any proxy in between must not cache responses. Stock data is live, and any caching layer will surface a stale quantity.
Base URL in production: https://www.888wholesale.co/api/wms/v1. A GET /health probe is available and returns { "status": "ok" } without authentication.
Atomic by design
2. Authentication & Roles
Tokens are issued through POST /desktop-login with the user's portal email and password. The endpoint is throttled per-IP and per-account; suspended and rejected accounts are refused before a token is minted. A successful login returns a raw wms_* token that is shown once, so store it securely. Tokens carry a 90-day lifetime by default; the client re-authenticates automatically on expiry.
Send the raw token on every subsequent call as Authorization: Bearer <token>. A new desktop login revokes only the previous auto-issued desktop token for the same user; other active tokens (for example a supervisor token when a worker signs in) stay valid.
# 1. Exchange credentials for a Bearer token
curl -X POST https://www.888wholesale.co/api/wms/v1/desktop-login \
-H "Content-Type: application/json" \
-d '{"email":"partner@example.com","password":"********"}'
# Response
# {
# "success": true,
# "token": "wms_a1b2c3...",
# "deviceSecret": "9f2c...", // signs every later request, store securely
# "warehouseId": "whs_...", // the warehouse this token is bound to
# "warehouseName": "Main Warehouse",
# "userName": "Acme Logistics",
# "customerRole": "SUPERVISOR", // or "WORKER" / "FACTORY"
# "parentCustomerId": null,
# "companyName": "Acme Logistics",
# "canWrite": true, // false = VIEW-only on the bound warehouse
# "accessibleWarehouses": [ // switch targets; [] for workers
# { "id": "whs_...", "name": "Main Warehouse",
# "level": "EDIT", "isOwn": true }
# ]
# }
# 2. Use it on every subsequent call
curl https://www.888wholesale.co/api/wms/v1/stock \
-H "Authorization: Bearer wms_a1b2c3..."Role matrix
A token carries one of three roles, fixed at issuance and enforced server-side on every request. Supervisor-only endpoints return 403 { error: "Worker accounts cannot perform this action." } for worker tokens. A FACTORY token is a different credential class entirely: it is scoped to a factory, not a warehouse, and can only reach the two /factory/* routes described in section 8.
| Capability | SUPERVISOR | WORKER | FACTORY |
|---|---|---|---|
Read stock levels (GET /stock) | Yes | Yes | 403 |
Read event log (GET /transactions) | All warehouse activity | Own issuances only | 403 |
Mutate stock (/stock/update, /stock/set, /stock/receive) | Yes, with EDIT | 403 | 403 |
| Approve alerts, receive ASNs, forecast decisions | Yes, with EDIT | 403 | 403 |
| Manage webhooks & worker accounts | Yes | 403 | 403 |
| Switch warehouse, company overview | Yes | 403 | 403 |
Production worklist (/factory/*) | 403 | 403 | Yes |
Write permission is separate from role
Supervisor scope grants access to the write endpoints; whether a given token may actually use them is a second, per-warehouse decision. Each token carries canWrite, derived from the holder's access level on the bound warehouse: EDIT permits mutation, VIEW does not. A VIEW-scoped token reads everything normally and fails every write with 403 { reason: "view_only" }. Read the canWrite flag from the login response and render a read-only state rather than letting the user attempt a mutation that is going to be refused.
Worker isolation
GET /transactionsreturns only the calling worker's own issuances when called with a worker token. A worker can never observe another worker's activity, even by changing query parameters: the privacy boundary is enforced inside the API, not in the desktop UI.Device binding and request signing
A token issued to the desktop client is bound to the machine that requested it and must present X-Machine-Fingerprint on every call. A missing fingerprint is refused, not tolerated, so a copied token cannot be replayed elsewhere simply by omitting the header. A mismatched fingerprint locks the token and notifies the account owner, who signs in again to receive a fresh one.
Desktop logins additionally receive a per-login deviceSecret. Each request is signed HMAC-SHA256(deviceSecret, "{timestamp}\n{METHOD}\n{pathname}") and sent as X-Device-Signature alongside X-Device-Timestamp, accepted within a five-minute clock skew. A captured token plus fingerprint is therefore still not enough to impersonate the device.
Server-to-server keys are unaffected
Failure reasons
Auth failures carry a machine-readable reason beside the human-readable error, so a client can distinguish "sign in again" from "this will keep failing until an admin acts".
| reason | Status | What it means |
|---|---|---|
| missing / invalid | 401 | No Bearer header, or the token is not recognised. |
| expired / revoked | 401 | Past its lifetime, or revoked in the portal. Sign in again. |
| fingerprint_missing | 401 | A desktop token was presented without X-Machine-Fingerprint. |
| fingerprint_mismatch | 403 | Presented from a different machine. The token is now locked. |
| signature_invalid | 401 | Device signature absent, malformed, or outside the skew window. |
| view_only | 403 | Write attempted with VIEW access on the bound warehouse. |
| device_limit | 403 | Login from a new machine with every device seat in use. |
| device_revoked | 403 | This machine was deactivated by an administrator. |
3. Idempotency
Both POST /stock/update and POST /transactions accept an Idempotency-Key header. The server stores the response under the key for 24 hours and replays the cached payload on retry, so a network blip during a Shopify-driven deduction can never produce a double-deduct.
Keys are scoped to your warehouse, so the same string used against a second warehouse can never collide or leak across tenants.
# Safe retry: identical key + body returns the cached response
KEY=$(uuidgen)
curl -X POST https://www.888wholesale.co/api/wms/v1/stock/update \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{
"transactions": [
{ "sku": "TSHIRT-BLK-L", "delta": -5, "type": "SALE",
"reference": "shopify:order:1234" }
]
}'
# Re-sending with the same Idempotency-Key returns the original
# response body for up to 24 hours. No second mutation occurs.Pick stable keys per logical event
order:1234, or a crypto.randomUUID() stored alongside the order row), not one per HTTP attempt. If you re-roll the key on retry, the server treats it as a brand-new transaction and applies the delta again.4. Stock Model & Atomic Updates
Each SKU in a warehouse is a StockLevel with three numbers: currentQuantity, refillThreshold, and parLevel. The reported status is computed server-side:
| Status | Condition | Effect |
|---|---|---|
| CRITICAL | current ≤ refillThreshold | Flagged for replenishment; raises a ReplenishmentAlert |
| LOW | current < parLevel × 0.5 | Surfaced in dashboards as a warning |
| OK | Otherwise | No action required |
Alongside the coarse status, each level carries warningStage: 30, 20, or 10 once the balance falls into that percentage band of parLevel, and null while healthy. Use it to render graduated severity ahead of a threshold breach rather than waiting for the binary flip to CRITICAL.
Levels also carry their catalog identity: productId, productName, and variantName. Group size variants of one garment by productId rather than by parsing the SKU string, which is not a stable structure.
curl "https://www.888wholesale.co/api/wms/v1/stock?limit=500" \
-H "Authorization: Bearer wms_..."
# 200 OK
# {
# "levels": [
# {
# "sku": "TSHIRT-BLK-L",
# "current": 24,
# "threshold": 20,
# "par": 200,
# "status": "LOW",
# "warningStage": 10, // null while healthy
# "lastUpdatedAt": "2026-06-07T08:11:04.000Z",
# "productId": "prd_...", // group variants by this, never by SKU
# "productName": "Heavyweight Tee Black",
# "variantName": "L"
# }
# ],
# "total": 128, "page": 1, "limit": 500
# }Delta accumulation
Hot-path mutations use POST /stock/update with a batch of signed delta values typed SALE, RETURN, DAMAGE, or ADJUSTMENT. The whole batch is validated before anything is written: duplicate SKUs are accumulated, not collapsed, and any line that would drive the projected balance below zero rejects the entire batch with 422. A partial oversell can never slip through.
curl -X POST https://www.888wholesale.co/api/wms/v1/stock/update \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0d8e...c1b2" \
-d '{
"transactions": [
{ "sku": "TSHIRT-BLK-L", "delta": -3, "type": "SALE" },
{ "sku": "TSHIRT-BLK-L", "delta": -2, "type": "SALE" },
{ "sku": "HOODIE-NVY-M", "delta": 4, "type": "RETURN",
"reference": "rma:RMA-2026-0117" }
]
}'
# 200 OK
# {
# "processed": 3,
# "levels": [
# { "sku": "TSHIRT-BLK-L", "currentQuantity": 95, "belowThreshold": false },
# { "sku": "HOODIE-NVY-M", "currentQuantity": 14, "belowThreshold": false }
# ]
# }Absolute writes have a separate endpoint
POST /stock/set only for stocktake reconciliation. It takes absolute counts, not deltas, and records an AUDIT transaction with the difference it computed. Because it overwrites rather than accumulates, never call it from a webhook or a retry loop where an out-of-order replay would resurrect a stale count. POST /stock/receive is the one-line shorthand for positive-delta arrivals and also resolves a linked ORDERED replenishment alert.# Stocktake: set counted quantities. Server computes each delta and
# writes an AUDIT transaction carrying your note.
curl -X POST https://www.888wholesale.co/api/wms/v1/stock/set \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-d '{
"levels": [
{ "sku": "TSHIRT-BLK-L", "quantity": 118, "note": "Cycle count W23" },
{ "sku": "HOODIE-NVY-M", "quantity": 64, "note": "Cycle count W23" }
]
}'Every successful mutation writes a StockTransaction row with quantityAfter captured after the change. The source field is set automatically: API for HTTP traffic, DESKTOP for receives via the desktop client, and PORTAL for in-browser worker issuances.
5. VMI Replenishment
Vendor-Managed Inventory runs as a periodic replenishment sweep. Levels that have recently moved are re-evaluated against their refillThreshold; a breach opens a ReplenishmentAlertand fires the partner's subscribed webhooks. No per-event email is sent: replenishment signals surface in the desktop client, the portal, and the API.
Alert lifecycle: OPEN → ACKNOWLEDGED → ORDERED → RESOLVED. The desktop client and supervisor portal both call PATCH /alerts/[id] with { "action": "approve", "quantity": N } to move the alert to ORDERED and create or update the linked ReplenishmentOrder. Re-PATCHing an already-ordered alert returns 200 { alreadyOrdered: true }, so retries never surface as a 4xx.
Each alert carries an alertType that states what 888 has to do about it. REPLENISHMENT means the goods exist and get called off to your warehouse; PRODUCTION means they do not exist yet and manufacturing has to start, which is the longer lead time of the two. Surface the distinction rather than treating every alert as a shipment: it is the difference between days and weeks.
An alert also closes on its own. If stock recovers above the threshold before anyone acts, the next sweep resolves the open alert instead of leaving a stale one for a supervisor to dismiss by hand.
Reorder recommendations
GET /recommendations?days=30 returns a reorder-point analysis per SKU. Defaults: 30-day lookback (capped at 90), a 7-day lead time, and 14 days of safety stock. It is pipeline-aware: goods already in transit count against the shortfall, so a second shipment is never recommended while the first is still on the way. Each entry includes velocityPerDay, reorderPoint, recommendedOrder, achievableOrder (capped by 888's own central stock), and a stock-out projection (daysOfStockLeft, depletionDate, orderByDate).
curl https://www.888wholesale.co/api/wms/v1/recommendations?days=30 \
-H "Authorization: Bearer wms_..."
# 200 OK. recommendations sorted by recommendedOrder DESC
# {
# "recommendations": [
# {
# "sku": "TSHIRT-BLK-L",
# "name": "Heavyweight Tee Black L",
# "currentStock": 24,
# "ourStock": 1800,
# "inProduction": 0,
# "inTransit": 120,
# "consumedInPeriod": 240,
# "velocityPerDay": "8.00",
# "reorderPoint": 168,
# "recommendedOrder": 240,
# "achievableOrder": 240,
# "daysOfStockLeft": 3,
# "orderByDate": "2026-06-08",
# "periodDays": 30
# }
# ],
# "consumptionTrend": [
# { "date": "05-01", "units": 9 },
# { "date": "05-02", "units": 11 }
# ]
# }Beyond reactive alerts, a forward-looking procurement forecast proposes quarter and half-year restock quantities from run-rate and months-of-cover. It is surfaced in the desktop Procurement Plan tab and read through GET /forecast (see the API reference).
6. ASN Receiving
Advance Shipment Notices model the inbound leg of the VMI loop. 888 dispatch creates an AdvancedShipmentNotice with one line per SKU (POST /shipments, dispatch-only); the partner receives it via POST /shipments/{id}/receive. The receive call is the 1-scan pallet receive the desktop client opens when a barcode prefixed ASN- is scanned.
The receive endpoint runs a pre-flight check before applying anything: every line SKU must already exist in the product catalog. Unknown SKUs return 422 { unregisteredSkus: [...] }, so the desktop can show a clear "ask admin to register these" prompt without a wasted write. Duplicate SKUs in the request body are rejected with 422 rather than silently merged.
Atomic receive with variance
StockLevel, writes a RECEIVE transaction, records the received quantity and the variance against what was expected, and then moves the ASN to RECEIVED or PARTIALLY_RECEIVED. If the ASN is linked to a ReplenishmentOrder, its alert resolves in the same operation.# Auto-accept all expectedQty (no line body needed)
curl -X POST https://www.888wholesale.co/api/wms/v1/shipments/asn_abc/receive \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-d '{ "stationId": "PACK-01" }'
# With variance (short on TSHIRT-BLK-L by 2 units)
curl -X POST https://www.888wholesale.co/api/wms/v1/shipments/asn_abc/receive \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-d '{
"stationId": "PACK-01",
"lines": [
{ "sku": "TSHIRT-BLK-L", "receivedQty": 98 },
{ "sku": "HOODIE-NVY-M", "receivedQty": 50 }
]
}'
# 200 OK
# { "success": true, "asnNumber": "ASN-00042",
# "status": "PARTIALLY_RECEIVED",
# "lines": [
# { "sku": "TSHIRT-BLK-L", "receivedQty": 98, "variance": -2, "quantityAfter": 122 },
# { "sku": "HOODIE-NVY-M", "receivedQty": 50, "variance": 0, "quantityAfter": 64 }
# ]
# }7. Webhooks & Security
Partners can subscribe to alert.triggered, order.shipped, and order.delivered via POST /webhooks. The signing secret is returned once in the response, so store it immediately; later GET /webhooks calls return only the metadata. At rest the secret is encrypted before it is written to the database.
Each event is POSTed once with a short timeout and is best-effort. Make your receiver idempotent and reconcile against the GET endpoints (stock, alerts, shipments) as the authoritative state. The delivery carries two headers, X-WMS-Signature (formatted sha256=<hex>) and X-WMS-Event, and a JSON body of { event, ...fields, timestamp }. The signature is an HMAC-SHA256 over the exact bytes you receive.
curl -X POST https://www.888wholesale.co/api/wms/v1/webhooks \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.partner.com/888-wms",
"events": ["alert.triggered", "order.shipped"]
}'
# 201 Created. Capture "secret" now, you will not see it again.
# {
# "id": "wh_...",
# "url": "https://hooks.partner.com/888-wms",
# "events": ["alert.triggered", "order.shipped"],
# "createdAt": "2026-05-24T09:00:00.000Z",
# "secret": "f3e9...a1b2"
# }Verify every payload
Recompute the HMAC over the raw body and compare it with a timing-safe check. Reject anything that does not match. Registration also rejects any URL that is not HTTPS or that resolves to a private, loopback, or reserved address, and the same check runs again at delivery time, so only public HTTPS endpoints ever receive a payload.
// Verify an incoming 888 WMS webhook on the partner side
import crypto from "node:crypto";
function verify(rawBody: string, signatureHeader: string, secret: string) {
// Header arrives as "sha256=<hex>"
const provided = signatureHeader.replace(/^sha256=/, "");
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // sign the exact bytes you received
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(provided);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Treat the API as the source of truth
GET /stock, GET /alerts, and GET /shipments. Verify the signature on every request, ignore anything that fails the check, and poll as a safety net.8. Multi-Warehouse & Factory Access
A token is bound to exactly one warehouse. Everything in the sections above, stock, alerts, ASNs, recommendations, resolves against that one binding, which is what keeps the tenancy boundary simple. Two mechanisms exist for accounts whose reach is wider than a single warehouse.
Switching the bound warehouse
A director may hold access to warehouses they do not own, granted per warehouse as VIEW or EDIT. The login response lists them in accessibleWarehouses. POST /switch-warehouse re-points the session: it verifies the access grant, revokes the current token, and mints a fresh one bound to the target with canWriteset from that warehouse's access level.
The old token dies at the moment of the switch
token and a new deviceSecret; the previous pair stops working immediately. Finish or abandon in-flight writes before switching, swap both credentials atomically, and discard every cached payload, because the data you are holding belongs to the previous warehouse. A request that races the swap fails with 401 and, if you treat that as a session expiry, logs the user out for no reason.curl -X POST https://www.888wholesale.co/api/wms/v1/switch-warehouse \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-d '{ "targetWarehouseId": "whs_9f21" }'
# 200 OK — the previous token is now revoked
# {
# "success": true,
# "token": "wms_new...",
# "deviceSecret": "7c4a...",
# "warehouseId": "whs_9f21",
# "warehouseName": "Barcelona DC",
# "companyName": "Acme Logistics",
# "canWrite": false, // VIEW grant on this one
# "accessibleWarehouses": [ ... ]
# }
# 403 { "error": "...", "reason": "no_access" } when no grant exists.Company-wide overview
GET /company/warehouses returns every warehouse the calling director holds a grant on, each with its stock levels, owner, and storage mode, without switching the session. It is strictly read-only and exists so a director can see shared context in one view. It returns grants only, not every warehouse in the company, so it can never widen visibility beyond what was explicitly given. A caller who belongs to no company gets { "company": null, "warehouses": [] }.
Factory production worklist
A supplier factory can be issued its own login. The resulting FACTORY token is scoped to a factory rather than a warehouse, so none of the stock endpoints apply to it; it reaches exactly two routes. GET /factory/worklist lists the garment lines that factory has to manufacture, drawn from its assigned orders once they are confirmed and at least part-paid, so production never starts on an unfunded order.
The worklist carries no commercial data
PATCH /factory/items/{id} reports progress on one line through ORDERED → IN_PRODUCTION → MANUFACTURED → SHIPPED, with an optional note. Ownership is enforced by joining back to the parent order's factory, and a mismatch returns 404 rather than 403so the endpoint cannot be used to enumerate other factories' work. Lines on shipped or cancelled orders are no longer editable.
curl -X PATCH https://www.888wholesale.co/api/wms/v1/factory/items/itm_5b1c \
-H "Authorization: Bearer wms_..." \
-H "Content-Type: application/json" \
-d '{ "status": "IN_PRODUCTION", "note": "Cutting started, line 3" }'
# Older clients may send the legacy boolean instead:
# { "done": true } → MANUFACTURED
# { "done": false } → ORDERED9. API Reference
Every route below is prefixed with /api/wms/v1. Unless noted, every call requires Authorization: Bearer <token> and supervisor scope.
Auth & Health
Probe. No authentication required.
/healthIssue a Bearer token from portal credentials. Throttled per-IP and per-account.
/desktop-loginInitiate a password reset for desktop logins.
/desktop-forgot-passwordStock
Paginated stock levels (?sku=, ?limit=, ?page=). Worker tokens are read-only.
/stockAtomic batch of signed deltas. Accepts Idempotency-Key. Supervisor only.
/stock/updateAbsolute stocktake. Records an AUDIT transaction with the computed delta. Supervisor only.
/stock/setSingle-SKU positive receive; auto-resolves any linked ORDERED alert. Supervisor only.
/stock/receiveTransactions
Event log. Supervisor sees all warehouse activity; worker sees their own WorkerIssuance rows only.
/transactionsSingle IN / OUT / ADJUST mutation from the desktop client. Accepts Idempotency-Key. Supervisor only.
/transactionsReplenishment
Open, acknowledged, and ordered alerts for this warehouse. Supervisor only.
/alertsApprove an alert. Body: { "action": "approve", "quantity"?: N }. Idempotent on retry.
/alerts/{id}Reorder-point and velocity recommendations. ?days=30 (default 30, max 90). Supervisor only.
/recommendationsProcurement Forecast
Active proposal plus a live months-of-cover snapshot for the warehouse. Supervisor only.
/forecastRecompute the proposal from the latest run-rate. Supervisor only.
/forecast/regenerateApprove a proposal (optionally with a chosen horizon and per-line quantities). Supervisor only.
/forecast/{id}/approveReject a proposal with a reason. Supervisor only.
/forecast/{id}/rejectShipments (ASN)
List active ASNs for this warehouse (?status=IN_TRANSIT|PENDING|...).
/shipmentsCreate an ASN (888 dispatch). A unique asnNumber is enforced server-side.
/shipments1-scan receive. Atomic stock upsert plus variance plus alert resolution.
/shipments/{id}/receiveWebhooks
List active partner webhooks. Supervisor only.
/webhooksRegister a webhook URL (HTTPS only, address-checked). Returns the signing secret once.
/webhooksSoft-delete a webhook by id (?id=).
/webhooksSession & Company
Re-bind the session to another accessible warehouse. Body: { "targetWarehouseId": "..." }. Revokes the calling token and returns a new one. Directors only.
/switch-warehouseRead-only overview of every warehouse the caller holds a grant on, with stock. Directors only.
/company/warehousesFactory Production
Garment lines to manufacture on confirmed, part-paid orders. No pricing fields. FACTORY tokens only.
/factory/worklistReport a line's production status. Body: { "status": "IN_PRODUCTION", "note"?: "..." }. FACTORY tokens only.
/factory/items/{id}Worker Accounts
List child worker accounts. Supervisor only.
/workersCreate a worker account inline (name, email, password). Supervisor only.
/workersSoft-delete a worker (suspends the user, revokes their tokens, preserves audit history).
/workers/{id}