WMS_DEV_DOCS

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

Every mutation is validated server-side and applied atomically. Stock writes use relative deltas and can never drive a balance below zero, so a rejected or partially-invalid request leaves your levels untouched.

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 or rejected accounts and non-customer roles are refused before a token is minted. A successful login returns a raw wms_* token that is shown once, so store it securely. Tokens expire after 90 days.

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.

bash
# 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...",
#   "warehouseName": "Main Warehouse",
#   "userName": "Acme Logistics",
#   "customerRole": "SUPERVISOR",   // or "WORKER"
#   "parentCustomerId": null,
#   "companyName": "Acme Logistics"
# }

# 2. Use it on every subsequent call
curl https://www.888wholesale.co/api/wms/v1/stock \
  -H "Authorization: Bearer wms_a1b2c3..."

Desktop tokens are device-bound

A token activated by the desktop client is bound to the machine that first uses it. Presenting the same token from a different device locks it and notifies the account owner, who simply signs in again to receive a fresh token. Server-to-server integration keys that never present a device fingerprint are not subject to this binding and keep working normally.

Role matrix

Each token is either supervisor-scoped (full read plus write) or worker-scoped (read-only). The role is fixed when the token is issued and enforced server-side on every request. Supervisor-only endpoints return 403 { error: "Worker accounts cannot perform this action." } for worker tokens.

CapabilitySUPERVISORWORKER
Read stock levels (GET /stock)YesYes
Read event log (GET /transactions)All warehouse activityOwn issuances only
Mutate stock (/stock/update, /stock/set, /stock/receive)Yes403
Approve replenishment alertsYes403
Receive ASNsYes403
Manage webhooks & worker accountsYes403

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.

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.

bash
# 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

Generate one key per business event (for example 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:

StatusConditionEffect
CRITICALcurrent ≤ refillThresholdFlagged for replenishment; raises a ReplenishmentAlert
LOWcurrent < parLevel × 0.5Surfaced in dashboards as a warning
OKOtherwiseNo action required

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.

bash
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

Use POST /stock/set only for stocktake reconciliation. It records an AUDIT transaction with the computed delta and is not idempotent, so never call it from a webhook or a retry loop. POST /stock/receive is the one-line shorthand for positive-delta arrivals and also resolves a linked ORDERED replenishment alert.

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.

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).

bash
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

The receive is atomic. For each line it updates the 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.
bash
# 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.

bash
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.

typescript
// 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

A webhook is a fast nudge, not a guarantee. If your endpoint is briefly unreachable, no data is lost: the same state is always readable from 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. 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.

GET/health

Issue a Bearer token from portal credentials. Throttled per-IP and per-account.

POST/desktop-login

Initiate a password reset for desktop logins.

POST/desktop-forgot-password

Stock

Paginated stock levels (?sku=, ?limit=, ?page=). Worker tokens are read-only.

GET/stock

Atomic batch of signed deltas. Accepts Idempotency-Key. Supervisor only.

POST/stock/update

Absolute stocktake. Records an AUDIT transaction with the computed delta. Supervisor only.

POST/stock/set

Single-SKU positive receive; auto-resolves any linked ORDERED alert. Supervisor only.

POST/stock/receive

Transactions

Event log. Supervisor sees all warehouse activity; worker sees their own WorkerIssuance rows only.

GET/transactions

Single IN / OUT / ADJUST mutation from the desktop client. Accepts Idempotency-Key. Supervisor only.

POST/transactions

Replenishment

Open, acknowledged, and ordered alerts for this warehouse. Supervisor only.

GET/alerts

Approve an alert. Body: { "action": "approve", "quantity"?: N }. Idempotent on retry.

PATCH/alerts/{id}

Reorder-point and velocity recommendations. ?days=30 (default 30, max 90). Supervisor only.

GET/recommendations

Procurement Forecast

Active proposal plus a live months-of-cover snapshot for the warehouse. Supervisor only.

GET/forecast

Recompute the proposal from the latest run-rate. Supervisor only.

POST/forecast/regenerate

Approve a proposal (optionally with a chosen horizon and per-line quantities). Supervisor only.

POST/forecast/{id}/approve

Reject a proposal with a reason. Supervisor only.

POST/forecast/{id}/reject

Shipments (ASN)

List active ASNs for this warehouse (?status=IN_TRANSIT|PENDING|...).

GET/shipments

Create an ASN (888 dispatch). A unique asnNumber is enforced server-side.

POST/shipments

1-scan receive. Atomic stock upsert plus variance plus alert resolution.

POST/shipments/{id}/receive

Webhooks

List active partner webhooks. Supervisor only.

GET/webhooks

Register a webhook URL (HTTPS only, address-checked). Returns the signing secret once.

POST/webhooks

Soft-delete a webhook by id (?id=).

DELETE/webhooks

Worker Accounts

List child worker accounts. Supervisor only.

GET/workers

Create a worker account inline (name, email, password). Supervisor only.

POST/workers

Soft-delete a worker (suspends the user, revokes their tokens, preserves audit history).

DELETE/workers/{id}