CanopyDocs

Conventions

Idempotency, error schema, rate limits, pagination, filtering, and incremental sync — the rules that apply across every endpoint.

These rules apply across the whole API.

Idempotency

Every write endpoint (POST, PATCH, PUT and DELETE — creates, edits, holds, cancels, line changes, voids) accepts an Idempotency-Key header. Send a unique key (e.g. a UUID) per logical operation and you can safely retry on timeouts or network errors without the write landing twice. A request without the header behaves as it always has.

Idempotent create
curl -X POST https://api.staging.canopywms.com/api/orders \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: 6f1a8c2e-..." \
  -H "Content-Type: application/json" \
  -d '{ "...": "..." }'

Behavior:

SituationResult
First request with a keyRuns normally; the 2xx response is stored against the key.
Retry after successReturns the original status and body verbatim, with an Idempotent-Replay: true header.
Retry more than 24 hours after the first requestThe key has expired: the request runs again as a new operation (and reserves the key afresh). Retry within 24 hours if you need the replay.
Concurrent duplicate (first still in flight)409 — A request with this Idempotency-Key is already in progress. If the first request has been pending for more than 5 minutes it is presumed dead, and the duplicate takes over the key and runs instead.
Same key reused for a different operation (other method, URL, or a changed body), or by a different credential or a token with different scopes422 — Idempotency-Key was already used for a different operation. A replay only ever goes to the caller that made the original request.

Only successful (2xx) responses are cached, so a transient validation error never "poisons" a key.

A key is at most 255 characters. It lives in the partition of the client the write targets: a client-pinned credential's own client, or, for a tenant-wide credential, the clientId in the body (plus the orderId on a label buy). So a tenant-wide credential that reuses one key for two different clients has sent two different keys, and both requests run. Within a partition the key is bound to the operation (method, path, body) and to the caller (the credential, its role and its token scopes), which is what the 422 row above enforces.

Domain-level idempotency for purchase orders

Purchase-order creates are also idempotent on your external PO number — re-creating a PO with an externalPoNumber that already exists returns the existing PO rather than a duplicate, independent of the Idempotency-Key header. See Endpoints → Purchase orders.

Response envelope

Every success response is { "success": true, "data": ... }. List endpoints add a pagination block beside data; a few reads (order notes, blocked orders, client lookup, shipping services, and the billing lists of charges, adjustments, periods and rate cards) return a bare array in data with no pagination. GET /api/billing/invoices adds the block only when you send page (see Pagination). GET /api/billing/invoices/:id/download is the one success that is not JSON: it streams the file. Documented fields are a floor, not a ceiling: new fields may appear at any time, so ignore keys you don't recognise. The full shape of every response is in the Reference section.

Errors

Errors use a consistent JSON shape:

Error response
{
  "success": false,
  "error": "Human-readable message describing what went wrong"
}

A 400 from request validation adds a details map — field name to the messages for that field:

Validation error
{
  "success": false,
  "error": "Validation error",
  "details": { "updatedSince": ["Invalid datetime"] }
}

(The token endpoint is the one exception — see Authentication → Token errors.)

Conditions are distinguished by HTTP status:

StatusMeaning
400Validation error (malformed or missing fields — with a details map), or the operation is not allowed in the resource's current state: a status that forbids it, a cancel of a split order, a PO cancel after units were received or rejected, a line removed while the order is on an in-flight pick list, a label bought with a service the carrier account does not offer
401Missing or invalid token
403Token lacks the required scope, the endpoint isn't credential-callable, the operation is warehouse staff's call (a line reduced on an order that is being picked; a reship or carrier override, or any rate-card read, from a client-pinned credential), a client-pinned credential releasing a hold its own client did not place, the request came from an IP outside the credential's allowlist, or the body sets a warehouse-staff-only field (updateLines[].binId on a purchase order)
404Resource not found within your tenant/client scope — including an orderId passed to a shipping rate quote that doesn't resolve within it
409Conflict — an idempotency duplicate still in flight, a write that raced a concurrent change (re-read and retry), a write that collides with existing state (e.g. an active webhook endpoint already uses that URL, or an order created with an orderNumber its client already uses — existingOrderId names it), a label for an order that shipped inside another order's merged box (Order CC-… shipped in merged order CC-…'s box — reship or relabel CC-… instead), a line reduced or removed while a reship is re-sending it (This line is being re-sent for a reship, and a re-send ships whole — let it go out…), a cancel refused because another warehouse's open re-send is taking units off this order's box (<order numbers>'s re-send is taking units off this order's box <tracking> — cancel that re-send on its order first, or let it go out), a void refused because an open re-send is replacing that box (A re-send is open for this box — cancel the re-send first if the box is going out after all, or let the re-send go out.), or a cancel while a parcel already has carrier scans, a wholesale order has a live freight booking, or a wholesale shipment cycle is already complete; the body's error says which
422Idempotency key reused for a different operation
429Rate limit exceeded
5xxTransient server error — safe to retry with the same Idempotency-Key

A void refused because an open re-send is replacing the box also carries a machine-readable code field alongside error, so a credential or agent can branch on the reason without matching the message text:

Void refused
{
  "success": false,
  "error": "A re-send is open for this box — cancel the re-send first if the box is going out after all, or let the re-send go out.",
  "code": "OPEN_RESHIP"
}

Rate limits

Each credential has its own requests-per-minute limit. Responses carry the standard rate-limit headers so you can pace yourself:

x-ratelimit-limit: 600
x-ratelimit-remaining: 591
x-ratelimit-reset: 42

When you exceed the limit you get 429 with a retry-after header. The token endpoint (/oauth/token) is separately limited per source IP. Need a higher limit? Ask your CanopyWMS administrator — the per-credential limit is configurable.

Pagination

List endpoints are paginated with page and limit (max 500) query parameters, and return a pagination block:

List envelope
{
  "success": true,
  "data": [ /* ...rows... */ ],
  "pagination": { "page": 1, "limit": 50, "total": 1284, "totalPages": 26 }
}

Most lists also accept sortBy, sortOrder (asc/desc), and search.

The billing lists are the exception. GET /api/billing/invoices paginates only when you send page: limit then defaults to 25 (max 250) and the pagination block appears; without page, limit is ignored and every invoice in scope comes back. Charges, adjustments, periods and rate cards return the whole list in data with no page, limit or pagination.

Filtering & incremental sync

List endpoints expose resource-specific filters (status, date range, and exact external-reference lookups — see each resource under Endpoints). Date-range filters such as dateFrom / dateTo are calendar days; the shipment list and summary also take shippedFrom / shippedBefore as exact timestamps (see Endpoints → Shipments).

For incremental sync / changed-since polling, orders, purchase orders, and shipments accept an updatedSince parameter. It must be a full RFC 3339 datetime with a Z or a numeric offset (2026-06-29T00:00:00Z, 2026-06-28T19:00:00-05:00); a date-only value such as 2026-06-29 is a 400.

Orders and shipments also sort by updatedAt, so combine updatedSince with sortBy=updatedAt&sortOrder=asc to walk every change since your last poll:

Pull everything changed since the last sync
curl "https://api.staging.canopywms.com/api/orders?updatedSince=2026-06-29T00:00:00Z&sortBy=updatedAt&sortOrder=asc&limit=200" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Purchase orders accept updatedSince but not sortBy=updatedAt: the PO list only sorts by its own column names (created, expected, vendor, orderStatus, order, customer, tracking, note, inventoryStatus, receivedRatio), and any other sortBy — including updatedAt — is silently ignored, leaving the list ordered by createdAt descending. A delta walk on POs must therefore page the whole updatedSince window (every page, not "until the last row's timestamp") or poll with a generous overlap, rather than rely on updatedAt ordering.

Versioning

All endpoints live under the /api path. Event-type names and response fields are treated as a stable, additive contract — new fields and new webhook event types are added without breaking existing consumers, so build your integration to ignore unknown fields.

On this page