OccaStore Bridge API — v1
v1 Design Preview. Endpoints marked "Phase 1 ✓ Live" are deployed on staging and production. This is a design-preview reference and may be extended before v1 GA; the published page is
noindexuntil GA.
| Environment | Base URL |
|---|---|
| Staging (Design Preview / integration testing) | https://dev-api.occastore.com |
| Production (go-live traffic) | https://api.occastore.com |
API base path: /v1 on both. Credentials are per environment — see
Environments.
Not sure which integration you have? See Integration types (EDI/API over HTTPS vs drop-ship SFTP file exchange).
This reference covers the customer-facing order and read endpoints. Admin/operator endpoints are internal and are intentionally not documented here.
| Endpoint | Doc |
|---|---|
GET /status |
Public reachability probe (no auth) — Quickstart |
POST /v1/orders |
post-orders.md Phase 1 ✓ Live |
GET /v1/orders/:bridge_order_id |
get-order.md Phase 1 ✓ Live |
POST /v1/orders/:bridge_order_id/cancel |
cancel-order.md Phase 1 ✓ Live |
GET /v1/stock, GET /v1/stock/:sku |
stock.md Phase 1 ✓ Live |
GET /v1/catalogue, GET /v1/catalogue/:sku |
catalogue.md Phase 1 ✓ Live |
GET /v1/pricing, GET /v1/pricing/:sku |
pricing.md Phase 1 ✓ Live |
| Authentication (HMAC) | authentication.md |
| Error codes | error-codes.md |
Authentication
Every request is signed with HMAC-SHA256 over a canonical representation of the request, using four headers:
X-Bridge-Client-Id: cli_<...>
X-Bridge-Timestamp: <unix seconds>
X-Bridge-Nonce: <unique per request, e.g. a UUID>
X-Bridge-Signature: base64( HMAC_SHA256( secret, canonical ) )
where canonical is METHOD\nPATH\n<sha256-hex of raw body>\ntimestamp\nnonce.
The timestamp must be within 5 minutes of server time; each nonce is
single-use within a 10-minute window.
See authentication.md for the canonical-string rules, a fully-worked signing example, the seven authentication-failure reasons, the account-status codes, and the per-endpoint scopes. Don't duplicate that logic from here — it's the single source for signing.
Scopes
Each endpoint requires a per-integration scope grant (e.g. orders.write,
orders.read, orders.cancel, stock.read, catalogue.read,
pricing.read). Missing/disabled → 403 scope_disabled. The full mapping is in
authentication.md.
Idempotency
Write endpoints (POST /v1/orders, POST /v1/orders/:id/cancel) require an
Idempotency-Key header (a UUID you generate per logical operation).
- Same key + same body within 24 hours → the original stored response is replayed (no duplicate side effect).
- Same key + different body →
409 idempotency_conflict. - Key already used by a different account →
400 idempotency_key_conflict. - Missing header →
400 missing_idempotency_key.
Error envelope
All errors share one flat shape (never a nested object):
{ "error": "<http-class word>", "reason": "<stable_snake_code>" }
Some errors add a sibling field (e.g. field, conflicting_order_id).
Switch on reason — it is the stable, documented identifier; error is
just the HTTP-class word and may differ across surfaces for the same status.
Money is always integer pence (never a float/decimal). Every response
includes an X-Request-Id header for support correlation.
See error-codes.md for the complete reference.
Rate limiting
Per (account, scope) token bucket — defaults 20 requests/second burst,
60/minute, 10 000/day (tunable per account). Exceeding any limiter →
429 rate_limited with Retry-After and X-RateLimit-Limit /
X-RateLimit-Remaining / X-RateLimit-Reset headers. Full detail in
Rate limits.
Read endpoints & pagination
The read endpoints (stock, catalogue, pricing) serve cached,
eventually-consistent data. Each has a single-item form (/:sku) and a
bulk list form.
Single-item reads return the item object directly. The bulk list endpoints share one pagination envelope:
{
"items": [ /* ... */ ],
"page": 1,
"per_page": 100,
"total_pages": 5,
"total_items": 437,
"has_more": true
}
page— query parameter; default 1, minimum 1.per_page— query parameter; default 100, maximum 500.total_pages—0when there are no items, otherwiseceil(total_items / per_page).has_more—truewhenpage < total_pages.
See stock.md, catalogue.md, and pricing.md for each endpoint's response fields.
Quickstart — v1 Phase 1 ✓ Live
EDI/API only. If your onboarding mentions SFTP folders and CSV purchase orders, start at Integration types → Drop-ship SFTP.
From zero to a created-and-cancelled test order in a few minutes. This is the golden path; each step links the detail and the tooling that does the work for you.
Fastest path: if you'd rather not hand-roll anything yet, jump straight to the Terminal tester (
--checkproves your signing offline;--create/--canceldrive a real order) or the Postman collection (import it and every request is auto-signed). Come back here for the end-to-end shape.
1. Get your credentials
You need two values, issued to your account by OccaStore (contact your account manager — there is no self-serve signup during Design Preview). Credentials are per environment — staging and production use different Client IDs and secrets. See Environments.
- Client ID — sent on every request as
X-Bridge-Client-Id. - HMAC secret — the signing key. Keep it server-side; never put it in a URL, a query string, or client code.
2. The headers on every request
Every authenticated call carries four headers (writes add a fifth). Full rules in Authentication:
| Header | Value |
|---|---|
X-Bridge-Client-Id |
your Client ID |
X-Bridge-Timestamp |
current unix seconds (must be within ±5 min) |
X-Bridge-Nonce |
a fresh UUID per request (10-min replay window) |
X-Bridge-Signature |
the base64 HMAC (next step) |
Idempotency-Key |
writes only — a fresh UUID; your retry-safety handle |
3. Sign the request
X-Bridge-Signature = base64( HMAC_SHA256( secret, canonical ) ), where the
canonical string is five fields joined by \n:
METHOD\nPATH\nSHA256_HEX(body)\nTIMESTAMP\nNONCE.
Don't take our word for it — prove your signer against the published
signing vectors, and copy a ready-made signer from
reference signers (Node / Python / PHP / C#). The
bridge-api-tester.mjs --check runs this as an offline
self-test with zero setup.
4. Your first authenticated GET
Start with the public health probe (no auth), then a signed read:
GET /status # 200, no auth — confirms reachability
GET /v1/stock/YOUR-SKU # signed — confirms your HMAC is accepted
Any non-401 on the signed read means your signing works. See
Stock for the response shape and Rate limits
for the X-RateLimit-* headers you'll see.
5. Create a test order
POST /v1/orders with an Idempotency-Key and a JSON body (full contract in
Create order). A minimal body:
{
"reference": "QUICKSTART-001",
"items": [{ "sku": "YOUR-SKU", "quantity": 1, "unit_price_pence": 1000 }],
"ship_to": {
"name": "Example Customer",
"address1": "1 Example Street",
"town": "Leeds",
"postcode": "LS1 1AA",
"country": "GB"
}
}
Money is integer pence. On 201 you get back a bridge_order_id — keep it.
If you get a 4xx, look the reason up in Error codes (the
Remediation column tells you the next action).
6. Read it back, then cancel it
GET /v1/orders/{bridge_order_id} # see Get order
POST /v1/orders/{bridge_order_id}/cancel # see Cancel order
See Get order and Cancel order. A
successful cancel returns status: "cancelled". That's the full lifecycle —
you've signed, created, read, and cancelled.
Getting help
Every response — success or error — carries an X-Request-Id header. Quote
that X-Request-Id (and the reason if it was an error) when you contact your
account manager or support; it lets us find the exact request in our logs. The
bridge-api-tester.mjs and the
Postman collection are the quickest way to reproduce an issue
cleanly for a support request.
Integration types — v1
OccaStore supports two partner integration paths today. They share the same fulfilment backbone but use different transport. Start here before you read endpoint or file-format detail.
| Path | Transport | Status | Partner docs start here |
|---|---|---|---|
| EDI / API | Signed HTTPS (/v1) |
Phase 1 ✓ Live (staging + production) | Quickstart |
| Drop-ship SFTP | SFTP file exchange | Per-partner go-live | Drop-ship SFTP (below) |
If you are unsure which path your account uses, ask your OccaStore account manager. Credentials and folder layout are issued per integration; there is no self-serve signup during Design Preview.
EDI / API
Phase 1 ✓ Live on two HTTPS gateways:
| Environment | Base URL | Typical use |
|---|---|---|
| Staging | https://dev-api.occastore.com |
Design Preview, integration testing, conformance |
| Production | https://api.occastore.com |
Live traffic after go-live |
Full detail: Environments.
Use this path when your account has a Bridge Client ID and HMAC secret for the environment you are calling. Production credentials are issued at go-live and are not interchangeable with staging credentials.
What you can do
| Capability | Doc |
|---|---|
| Reachability probe (no auth) | GET /status — see Quickstart |
| Create and cancel orders | Create order, Cancel order |
| Poll order status | Get order, Order lifecycle |
| Read stock levels | Stock |
| Read catalogue metadata | Catalogue |
| Read trade pricing | Pricing |
Golden path
- Authentication — canonical signing rules and scopes.
- Quickstart — credentials → sign → first GET → test order.
- Conventions — integer pence, dates, references, idempotency (single source; do not re-derive from examples).
- Error codes — switch on stable
reasontokens.
Tooling: Terminal tester and Postman collection.
Set BRIDGE_BASE_URL / baseUrl to https://dev-api.occastore.com while
integrating on staging; switch to https://api.occastore.com with your
production credentials after go-live.
This path does not use SFTP folders or CSV purchase-order drops. If your onboarding pack mentions inbound PO files on SFTP, use the drop-ship section below instead.
Drop-ship SFTP
Per-partner go-live. SFTP file exchange is enabled by OccaStore when your account is activated and your integration is switched on. Until then, no files are picked up or generated even if you can reach the server.
Use this path when OccaStore exchanges CSV files over SFTP: you upload
purchase orders; OccaStore uploads stock and despatch feeds back. There is no
/v1 HMAC signing step for file exchange.
Your integration pack (issued by your account manager) is the authoritative spec for SFTP credentials, directory paths, filename patterns, and CSV column layout. This page describes the shape of the integration only; it does not replace your pack.
Overview
You ──SFTP upload──► Inbound PO CSV ──► OccaStore fulfilment
OccaStore ──SFTP upload──► Outbound stock CSV
OccaStore ──SFTP upload──► Outbound despatch CSV
Fulfilment runs inside OccaStore after your PO is accepted; there is no immediate SFTP acknowledgement file.
| Direction | Feed | Purpose |
|---|---|---|
| Inbound (you → OccaStore) | Purchase order CSV | Order lines to fulfil |
| Outbound (OccaStore → you) | Stock CSV | Availability and optional catalogue fields per mapped SKU |
| Outbound (OccaStore → you) | Despatch CSV | Tracking and despatch confirmation |
Outbound stock CSV column sources
Your integration pack lists the exact headers OccaStore emits. Each column maps a source to your chosen CSV header:
| Source kind | Examples | Meaning |
|---|---|---|
| SKU map | partnerItemNo, sku |
Partner item number or OccaStore's fulfilment-system SKU |
| Stock quantity | available, reserved, onOrder, dueIn, earliestExpiry |
Per-location stock cache ( available respects your configured basis and safety buffer ) |
| Catalogue | barcode, title, retailPrice, weight, taxRate, stockItemId, height, width, depth |
Catalogue data held for the SKU in OccaStore's fulfilment system |
| Location | location |
Your configured stock location name |
| Constant | { "constant": "GBP" } |
Fixed value in every row |
| Custom attribute | { "attribute": "Brand" } |
Any custom attribute recorded against the SKU in OccaStore's fulfilment system |
If a catalogue field is not cached for a SKU, the cell is left blank (the stock feed still uploads). Your account manager configures the column layout in your integration pack.
Getting set up
Your account manager provides:
- SFTP host, username, and authentication details (including host-key policy)
- The inbound directory where you place PO files
- The outbound directories where stock and despatch files will appear
- A written file specification: delimiter, encoding, headers, and column meanings
- Whether your account uses review or auto processing (see below)
Transport is SFTP (SSH file transfer), not FTPS or plain FTP. OccaStore connects to your server as the SFTP client, or uses a mutual drop zone, depending on your contract.
Review vs auto (stated in your pack):
- Review — after OccaStore accepts your PO file, an operator approves it before a fulfilment order is created.
- Auto — once ingest validation passes, OccaStore proceeds to fulfilment without operator approval.
Timing
Once your account is live, OccaStore polls SFTP on a short schedule agreed at setup. Your integration pack states the expected pickup and generation windows.
- Inbound PO files are picked up on that schedule after you place them.
- Stock files are generated on a periodic schedule from warehouse data, not immediately when you upload a PO.
- Despatch files appear after the order is despatched in fulfilment, not when the PO file lands.
After you place a test PO file, allow time for ingest, fulfilment, and the outbound windows before expecting stock or despatch CSVs.
What to test
- Place a test PO file in the inbound directory using the filename and columns from your integration pack.
- Confirm with your account manager that OccaStore received and accepted it.
- Check the outbound directories after the agreed windows for stock and despatch files.
- Open the CSVs and confirm quantities, SKUs, and tracking match your pack.
Reads over HTTPS
Some partners use SFTP for order files and the EDI/API path for stock, catalogue, or pricing reads. Ask your account manager whether your account has Bridge scopes for those endpoints.
Support
Layout, filename, or column changes go through your OccaStore account manager.
For /v1 signing, scopes, or error reasons, see
Authentication and Error codes.
Environments — v1 Phase 1 ✓ Live
OccaStore Bridge runs two EDI/API gateway environments. They are separate systems: different databases, different credentials, and different rate-limit counters. Always match your Client ID, HMAC secret, and base URL to the same environment.
| Staging | Production | |
|---|---|---|
| Base URL | https://dev-api.occastore.com |
https://api.occastore.com |
| Purpose | Design Preview integration testing | Live partner traffic |
| Credentials | Issued for onboarding / conformance | Issued at go-live (distinct from staging) |
| Reachability | GET https://dev-api.occastore.com/status |
GET https://api.occastore.com/status |
| When to use | Build and test your signer; conformance batteries | After your account manager confirms production go-live |
Both environments expose the same /v1 contract documented in this
reference. Behaviour and data are not shared — an order created on staging
does not appear on production, and vice versa.
Design Preview vs go-live
- Staging (
dev-api) is the default integration target during Design Preview. Use it for development, Postman/terminal-tester runs, and onboarding conformance tests unless told otherwise. - Production (
api) is deployed and available for per-partner go-live. Your account manager promotes you when integration testing is complete and issues new production credentials (client_id+ HMAC secret). Staging credentials do not work on production.
If you are unsure which environment your credentials target, ask your account manager before sending traffic.
Tooling variables
Set the gateway base URL in your tooling to match the environment:
| Tool | Variable | Staging | Production |
|---|---|---|---|
| Terminal tester | BRIDGE_BASE_URL |
https://dev-api.occastore.com |
https://api.occastore.com |
| Partner diagnostic | BRIDGE_BASE_URL |
https://dev-api.occastore.com |
https://api.occastore.com |
| Postman collection | baseUrl |
https://dev-api.occastore.com |
https://api.occastore.com |
See Terminal tester, Partner diagnostic, and Postman collection.
Drop-ship SFTP
Drop-ship partners use SFTP file exchange, not these HTTPS base URLs. SFTP hosts and paths are in your integration pack. Some accounts also have Bridge scopes for HTTPS reads — your account manager confirms whether those use staging or production credentials.
Support
Quote the X-Request-Id from the environment where the issue occurred, and
state whether the call was to dev-api or api.
Authentication — v1 Phase 1 ✓ Live
Every /v1 request is authenticated with an HMAC-SHA256 signature over a
canonical representation of the request. There are no bearer tokens, no
cookies, and no OAuth (OAuth is a later-phase design, not live) — each request
is independently signed.
Base URLs — see Environments:
- Staging:
https://dev-api.occastore.com - Production:
https://api.occastore.com
Credentials
When your integration is provisioned you receive:
- a Client ID (
cli_…) — public; sent on every request. - a secret — issued once at credential creation and never recoverable afterwards. Store it securely. To obtain a new secret, rotate the credential (which revokes the old one).
Request headers
Four headers on every /v1 request:
X-Bridge-Client-Id: cli_<your client id>
X-Bridge-Timestamp: <Unix time, integer seconds>
X-Bridge-Nonce: <unique value per request, e.g. a UUIDv4>
X-Bridge-Signature: <base64 HMAC-SHA256 — see below>
Signature
X-Bridge-Signature = base64( HMAC_SHA256( secret, canonical ) )
The canonical string is exactly five lines joined by a single \n
(newline, 0x0A):
<HTTP METHOD>
<request path, no query string>
<body hash>
<timestamp>
<nonce>
<HTTP METHOD>— uppercase, e.g.POST,GET.<request path, no query string>— e.g./v1/orders. Exclude any?….<body hash>— lowercase hex SHA-256 of the exact raw request body bytes. For a request with no body (e.g. aGET, or a cancel with an empty body) use the SHA-256 of the empty string:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.<timestamp>— the same integer sent inX-Bridge-Timestamp.<nonce>— the same value sent inX-Bridge-Nonce.
The secret is the HMAC key; the signature is the base64 of the raw HMAC digest.
Worked example
Take this exact request body for POST /v1/orders — minified, no trailing
newline. The body hash is computed over the exact bytes you send, so
whitespace matters; sign the same bytes you put on the wire.
{"reference":"PO-EXAMPLE-001","items":[{"sku":"WPI-CHOC-2KG","quantity":2,"unit_price_pence":4999}],"ship_to":{"name":"Jane Example","address1":"1 Example Way","town":"Leeds","postcode":"LS1 1AA","country":"GB"}}
Its lowercase-hex SHA-256 — which you can reproduce
(printf '%s' '<body>' | sha256sum) — is:
2dfdf33221c21e7353f774968a184f7b63785c37e95a34b023ba7b69cff221d3
With timestamp 1716123456 and nonce
b3f1a8c2-9d44-4e21-8a77-0c2e3f1a8c2e, assemble the canonical string from five
fields joined by a single \n (0x0A), no trailing newline. As a single
line, escaping the joins explicitly:
POST\n/v1/orders\n2dfdf33221c21e7353f774968a184f7b63785c37e95a34b023ba7b69cff221d3\n1716123456\nb3f1a8c2-9d44-4e21-8a77-0c2e3f1a8c2e
— which is, line by line:
POST
/v1/orders
2dfdf33221c21e7353f774968a184f7b63785c37e95a34b023ba7b69cff221d3
1716123456
b3f1a8c2-9d44-4e21-8a77-0c2e3f1a8c2e
Then sign the UTF-8 bytes of that canonical string with your issued secret:
X-Bridge-Signature = base64( HMAC_SHA256( <your issued secret>, canonical ) )
The signature value depends on your secret, so it is not reproduced here —
compute it with your secret. Send the same 1716123456 and
b3f1a8c2-… values as the X-Bridge-Timestamp and X-Bridge-Nonce headers
you signed over.
Timestamp window
X-Bridge-Timestamp must be within 300 seconds (5 minutes) of the
server's clock in either direction. Outside that window → 401
timestamp_skew. Keep your client clock NTP-synced.
Nonce / replay protection
X-Bridge-Nonce must be unique per request. A nonce is remembered for
600 seconds (10 minutes); reusing one within that window → 401
nonce_replay. The nonce is checked before the signature, so replaying a
previously-valid request is rejected as nonce_replay (not bad_signature).
Use a fresh UUID per request.
Authentication failures — the seven stages
Authentication runs seven checks in a fixed order; the first one that
fails determines the response. All seven return HTTP 401 with
{ "error": "unauthorized", "reason": "<below>" }:
| Order | reason |
Meaning |
|---|---|---|
| 1 | missing_headers |
One or more of the four X-Bridge-* headers is absent. |
| 2 | timestamp_skew |
Timestamp is non-integer/malformed, or outside the 5-minute window. |
| 3 | nonce_replay |
This nonce was already used within the last 10 minutes. |
| 4 | unknown_client |
The Client ID is not recognised. |
| 5 | revoked_credential |
The credential has been revoked. |
| 6 | expired_credential |
The credential has passed its expiry. |
| 7 | bad_signature |
The signature does not match the canonical string. |
On any 401, re-check your signing inputs (clock, nonce uniqueness, canonical
construction) before retrying. unknown_client / revoked_credential /
expired_credential indicate a credential problem that retrying will not fix
— rotate or contact support.
Account status
Once the signature is valid, the account itself is checked. These are distinct from the signing failures above:
| HTTP | reason |
Meaning |
|---|---|---|
| 403 | account_suspended |
Account is suspended (or onboarding has not reached go-live). |
| 401 | account_revoked |
Account has been revoked. |
| 403 | account_pending |
Account is provisioned but not yet activated. |
During onboarding, OccaStore may open a short conformance window on a
pending account so you can place a real test order and prove signing works
before go-live. While that window is open, authenticated /v1 calls are
permitted as if the account were active. Outside the window (or without a
window), pending accounts always receive 403 account_pending.
Note:
account_revokedreturns401, whileaccount_suspendedandaccount_pendingreturn403. This is deliberate: a revoked account is a terminated principal — like a revoked credential, it is an authentication failure (401), whereas suspended / pending accounts authenticate fine but are not currently permitted to act, an authorization state (403). Switch onreason, not on the HTTP status word.
Scopes
Beyond authentication, each endpoint requires a per-integration scope grant. The current scopes are:
| Scope | Grants |
|---|---|
orders.write |
POST /v1/orders |
orders.read |
GET /v1/orders/:bridge_order_id |
orders.cancel |
POST /v1/orders/:bridge_order_id/cancel |
stock.read |
GET /v1/stock, GET /v1/stock/:sku |
catalogue.read |
GET /v1/catalogue, GET /v1/catalogue/:sku |
pricing.read |
GET /v1/pricing, GET /v1/pricing/:sku |
A missing or disabled scope → 403 scope_disabled.
See error-codes.md for the complete error reference.
Authentication troubleshooting — v1 Phase 1 ✓ Live
Getting a 401? The reason in the response body tells you which signing stage
failed; the checklist below is ordered by how often each cause is the culprit.
Two tools resolve most cases without guessing: the
signing vectors (prove your signer offline, byte-for-byte),
the terminal tester's --check (an offline signing
self-test), and the partner diagnostic (full
step-by-step run with request_id capture for support).
First, read the reason
reason |
Which stage failed |
|---|---|
missing_headers |
A required X-Bridge-* header is absent. |
timestamp_skew |
Timestamp malformed, or outside the time window. |
nonce_replay |
The nonce was reused. |
unknown_client |
The Client ID isn't recognised. |
revoked_credential / expired_credential |
The credential is no longer valid. |
bad_signature |
The signature didn't match the canonical string. |
The checklist (most common first)
1. Sign the bytes you actually transmit → bad_signature
The single most common cause. You must hash the exact bytes you put on the wire — no pretty-printing, no re-serialisation after hashing, no added or stripped whitespace. If you build a JSON object, hash it, then let an HTTP library re-stringify or reformat it before sending, the transmitted bytes differ from what you hashed and the signature won't match.
Fix: capture the final request body once, hash those bytes, and send
those bytes. An empty body hashes to
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Prove your
signer against the signing vectors.
2. Stale or rotated secret → bad_signature
Worked yesterday, fails today? You may be signing with an old secret. After a credential rotation the previous secret stops working.
Fix: sign with your current secret. (A revoked credential returns
revoked_credential and an expired one expired_credential — distinct from
bad_signature.)
3. Clock skew → timestamp_skew
X-Bridge-Timestamp must be the current time in unix seconds, within
±5 minutes (300 seconds) of server time. A static/old timestamp, milliseconds
instead of seconds, or an unsynced clock all trip this.
Fix: send Math.floor(Date.now() / 1000); keep the host clock NTP-synced.
4. Nonce replay → nonce_replay
X-Bridge-Nonce must be a fresh UUID for every request. A nonce cannot be
reused within the 10-minute (600-second) replay window — a hard-coded nonce,
or a retry that reuses the previous one, will fail here.
Fix: generate a new UUID per request. (A retried write should reuse its
Idempotency-Key but still send a new nonce.)
5. Sign the path without the query string → bad_signature
Fails only on requests that carry a ?query? The PATH field of the canonical
string is the path excluding the query string — e.g. /v1/stock/SKU, not
/v1/stock/SKU?foo=bar.
Fix: strip everything from ? onward before building the canonical string.
6. Header names + canonical field order → missing_headers / bad_signature
Check the exact header names — X-Bridge-Client-Id, X-Bridge-Timestamp,
X-Bridge-Nonce, X-Bridge-Signature (writes also need Idempotency-Key and
Content-Type: application/json) — and that the canonical string is the five
fields in order, joined by \n:
METHOD
PATH
SHA256_HEX(body)
TIMESTAMP
NONCE
A wrong order or a missing field changes the signature.
Fix: copy a correct implementation from reference signers.
Rotating credentials (zero-downtime)
Your account can hold more than one active credential at a time — that's how
you rotate a secret without an outage. Each credential is an independent
(client_id, secret) pair, and signing with any active one authenticates.
To roll over with no downtime:
- Have a new credential issued — you now have two active.
- Deploy your signer with the new
client_id+ secret and confirm it works (the terminal tester--checkis the quickest proof). - Once all your traffic is on the new credential, have the old one revoked.
A revoked credential then returns 401 revoked_credential; an expired one
401 expired_credential. (Rotating the secret of a single credential in place
— same client_id, new secret — is also supported, but the two-credential
overlap above is the zero-downtime path.)
Still stuck?
Run the terminal tester --check — an offline signing
self-test. If that fails, your crypto is wrong (fix it against the
signing vectors). If it passes but a real call still
401s, the problem is on the live request — timestamp, nonce, secret, or
headers — work points 2–6 above. When you contact support, quote the
X-Request-Id from the response.
HMAC signing vectors — v1 Phase 1 ✓ Live
Generated file — do not hand-edit. Regenerate with
node scripts/gen-hmac-vectors.js. Every value below is produced from the live gateway signing code (shared/crypto.js) and re-derived + asserted in CI bytests/unit/hmac-vectors.test.js, so this page cannot silently drift from the implementation.
These are reproducible worked examples of the HMAC signature,
complete with the resulting X-Bridge-Signature. They use a public, obviously-fake
dummy secret so you can confirm your signer produces the exact same signature,
byte-for-byte, before you ever send a real request.
⚠️ The dummy secret authenticates nothing. Never sign a real request with it — use your own issued secret. These vectors exist only to validate your signing code.
Dummy secret: bridge-docs-example-secret-DO-NOT-USE
The canonical string is five fields joined by a single \n (0x0A), no trailing
newline: METHOD, the request PATH (no query string), the lowercase-hex SHA-256
of the exact raw body bytes, the TIMESTAMP, and the NONCE. An empty body hashes
to e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
Vector 1 — GET (empty body)
A read with no request body — e.g. GET /v1/stock/:sku. The body hash is the SHA-256 of the empty string.
| Field | Value |
|---|---|
| Method | GET |
| Path | /v1/stock/WPI-CHOC-2KG |
| Timestamp | 1716123456 |
| Nonce | d6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f |
| Body SHA-256 (hex) | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
Request body: (empty) — the body hash is the SHA-256 of the empty string.
Canonical string — five fields joined by a single \n (0x0A), no trailing newline. As one line with the joins escaped:
GET\n/v1/stock/WPI-CHOC-2KG\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n1716123456\nd6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f
— line by line:
GET
/v1/stock/WPI-CHOC-2KG
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1716123456
d6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f
X-Bridge-Signature = base64( HMAC_SHA256( dummy_secret, canonical ) ):
rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg=
Vector 2 — POST (JSON body)
A write with a JSON body — e.g. POST /v1/orders. The body hash is over the EXACT raw bytes sent (minified, no trailing newline); sign the same bytes you put on the wire.
| Field | Value |
|---|---|
| Method | POST |
| Path | /v1/orders |
| Timestamp | 1716123456 |
| Nonce | b3f1a8c2-9d44-4e21-8a77-0c2e3f1a8c2e |
| Body SHA-256 (hex) | 2dfdf33221c21e7353f774968a184f7b63785c37e95a34b023ba7b69cff221d3 |
Request body (minified, no trailing newline):
{"reference":"PO-EXAMPLE-001","items":[{"sku":"WPI-CHOC-2KG","quantity":2,"unit_price_pence":4999}],"ship_to":{"name":"Jane Example","address1":"1 Example Way","town":"Leeds","postcode":"LS1 1AA","country":"GB"}}
Canonical string — five fields joined by a single \n (0x0A), no trailing newline. As one line with the joins escaped:
POST\n/v1/orders\n2dfdf33221c21e7353f774968a184f7b63785c37e95a34b023ba7b69cff221d3\n1716123456\nb3f1a8c2-9d44-4e21-8a77-0c2e3f1a8c2e
— line by line:
POST
/v1/orders
2dfdf33221c21e7353f774968a184f7b63785c37e95a34b023ba7b69cff221d3
1716123456
b3f1a8c2-9d44-4e21-8a77-0c2e3f1a8c2e
X-Bridge-Signature = base64( HMAC_SHA256( dummy_secret, canonical ) ):
1YwQVYpLIql5XAup8updkYUtduSAHGgUmXU0aOgyrgQ=
Reference signers — v1 Phase 1 ✓ Live
Copy-paste HMAC signers in four languages. Each one reproduces the published signing vectors byte-for-byte — run it against the dummy secret and confirm you get the exact signatures below before you sign a real request. If your signature matches the vector, your signer is correct.
These implement the same canonical signing recipe as the live gateway
(shared/crypto.js). The Node snippet is re-executed against the vectors in CI
(tests/unit/signing-recipes.test.js), so it cannot silently drift from the
gateway. See Authentication for the full header/replay rules
and Signing vectors for the worked examples these
reproduce.
⚠️ The dummy secret
bridge-docs-example-secret-DO-NOT-USEauthenticates nothing — it exists only so you can validate your signing code. Sign real requests with your own issued secret.
The recipe (recap)
Build the canonical string — five fields joined by a single \n (0x0A),
no trailing newline:
METHOD
PATH (no query string)
SHA256_HEX(body) (lowercase hex; empty body → e3b0c4…b855)
TIMESTAMP (unix seconds)
NONCE (uuid)
then X-Bridge-Signature = base64( HMAC_SHA256( secret, canonical ) ).
Each signer below, run with the dummy secret, must produce these exact signatures (the signing vectors):
| Vector | Expected X-Bridge-Signature |
|---|---|
GET /v1/stock/WPI-CHOC-2KG (empty body) |
rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg= |
POST /v1/orders (example JSON body) |
1YwQVYpLIql5XAup8updkYUtduSAHGgUmXU0aOgyrgQ= |
Node.js
Node ≥ 14 — standard library only (node:crypto).
const crypto = require('node:crypto');
// SHA-256 of the raw request body, lowercase hex.
// Empty body → the well-known e3b0c4…b855 constant.
function computeBodyHash(body) {
return crypto.createHash('sha256').update(body ?? '', 'utf8').digest('hex');
}
// Returns the base64 X-Bridge-Signature for one request.
function signRequest({ secret, method, path, body, timestamp, nonce }) {
const bodyHash = computeBodyHash(body);
const canonical = [method, path, bodyHash, timestamp, nonce].join('\n');
return crypto.createHmac('sha256', secret).update(canonical, 'utf8').digest('base64');
}
// Example:
// signRequest({ secret: 'bridge-docs-example-secret-DO-NOT-USE', method: 'GET',
// path: '/v1/stock/WPI-CHOC-2KG', body: '', timestamp: '1716123456',
// nonce: 'd6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f' })
// === 'rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg='
Python
Python 3 — standard library only (hashlib, hmac, base64).
import hashlib
import hmac
import base64
def compute_body_hash(body: str) -> str:
"""SHA-256 of the raw request body, lowercase hex. Empty body -> e3b0c4...b855."""
return hashlib.sha256(body.encode("utf-8")).hexdigest()
def sign_request(secret: str, method: str, path: str, body: str,
timestamp: str, nonce: str) -> str:
body_hash = compute_body_hash(body)
canonical = "\n".join([method, path, body_hash, timestamp, nonce])
digest = hmac.new(secret.encode("utf-8"),
canonical.encode("utf-8"),
hashlib.sha256).digest()
return base64.b64encode(digest).decode("ascii")
if __name__ == "__main__":
sig = sign_request("bridge-docs-example-secret-DO-NOT-USE", "GET",
"/v1/stock/WPI-CHOC-2KG", "", "1716123456",
"d6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f")
assert sig == "rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg=", sig
print(sig)
PHP
PHP ≥ 7 — standard library only (hash, hash_hmac, base64_encode).
<?php
// SHA-256 of the raw request body, lowercase hex. Empty body -> e3b0c4...b855.
function compute_body_hash(string $body): string {
return hash('sha256', $body);
}
function sign_request(string $secret, string $method, string $path,
string $body, string $timestamp, string $nonce): string {
$bodyHash = compute_body_hash($body);
$canonical = implode("\n", [$method, $path, $bodyHash, $timestamp, $nonce]);
// raw_output=true -> raw binary HMAC, then base64-encode it.
return base64_encode(hash_hmac('sha256', $canonical, $secret, true));
}
$sig = sign_request('bridge-docs-example-secret-DO-NOT-USE', 'GET',
'/v1/stock/WPI-CHOC-2KG', '', '1716123456',
'd6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f');
assert($sig === 'rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg=');
echo $sig, PHP_EOL;
C#
.NET 6+ — standard library only (System.Security.Cryptography). Uses
SHA256.HashData and Convert.ToHexString (both .NET 5+).
using System;
using System.Security.Cryptography;
using System.Text;
public static class BridgeSigner
{
// SHA-256 of the raw request body, lowercase hex. Empty body -> e3b0c4...b855.
public static string ComputeBodyHash(string body)
{
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(body ?? string.Empty));
return Convert.ToHexString(hash).ToLowerInvariant();
}
public static string SignRequest(string secret, string method, string path,
string body, string timestamp, string nonce)
{
string bodyHash = ComputeBodyHash(body);
string canonical = string.Join("\n", method, path, bodyHash, timestamp, nonce);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
byte[] signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
return Convert.ToBase64String(signature);
}
public static void Main()
{
string sig = SignRequest("bridge-docs-example-secret-DO-NOT-USE", "GET",
"/v1/stock/WPI-CHOC-2KG", "", "1716123456",
"d6f5e4c3-1a2b-4c3d-8e9f-0a1b2c3d4e5f");
Console.WriteLine(sig); // rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg=
}
}
Verifying your signer
Run any snippet above with the dummy secret and the GET-vector inputs; it must
print rve5pdeYT2mMlDyuj14q81YCndcI4zc6snPhn622NAg=. Then swap in your own
issued secret and the real request fields. If the signature matches the vector
but the gateway returns 401, check the authentication rules:
the timestamp must be within ±5 minutes, the nonce must be unused (10-minute
replay window), and the path must exclude the query string.
Drift-guard coverage. The Node snippet on this page is extracted and executed against the published vectors in CI (
tests/unit/signing-recipes.test.js), so it can never drift from the gateway. The Python, PHP and C# snippets are verified against the same published vectors (their output is the vector signature, shown inline), but a runtime-matrix CI job that executes them in their own runtimes is a tracked follow-up (those runtimes aren't available in the Node/jest test environment).
Try it from your terminal — v1 Phase 1 ✓ Live
A zero-dependency Node script that signs and sends real Bridge requests — the fastest way to confirm your HMAC signing works end-to-end. Download it, set a few environment variables, and run.
Download: bridge-api-tester.mjs
Pure Node ≥ 20 (built-in fetch + node:crypto), no npm install. It uses the
exact canonical signing recipe from the signing vectors and
reference signers, so if it works, your own signer should
too.
Configure
All configuration is via environment variables — the secret is never passed on the command line and is never printed:
| Variable | Purpose |
|---|---|
BRIDGE_BASE_URL |
Gateway base URL — see Environments (https://dev-api.occastore.com for staging integration; https://api.occastore.com after go-live) |
BRIDGE_CLIENT_ID |
Your issued Client ID (required to sign) |
BRIDGE_SECRET |
Your issued HMAC secret — the signing key (required to sign) |
BRIDGE_SKU |
Line SKU for --create (default EXAMPLE-SKU) |
BRIDGE_PRICE |
Unit price in integer pence (default 1000) |
BRIDGE_REFERENCE |
Order reference, ≤ 60 chars [A-Za-z0-9._-] (default EXAMPLE-<time>) |
Run
--check (default) — prove your signing, create nothing:
node bridge-api-tester.mjs
With no BRIDGE_BASE_URL, this runs only the offline signing self-test: it
reproduces the published signing vectors locally (no credentials, no network). If
that passes, your crypto is correct. Set BRIDGE_BASE_URL to also probe
GET /status, and add BRIDGE_CLIENT_ID + BRIDGE_SECRET for a non-gating HMAC
auth probe (any non-401 proves your signature is accepted).
--create — make a real authenticated order:
BRIDGE_BASE_URL=https://dev-api.occastore.com \
BRIDGE_CLIENT_ID=your_client_id \
BRIDGE_SECRET=your_secret \
node bridge-api-tester.mjs --create
On 201 it prints the bridge_order_id. A non-201 prints the reason — look
it up on the Error codes page for the remediation.
--cancel — cancel that order:
node bridge-api-tester.mjs --cancel <bridge_order_id>
Exit codes: 0 ok · 1 a check/op failed · 2 misconfiguration.
For full step-by-step troubleshooting (stock, catalogue, pricing, optional
create, and a SHARE WITH OCCASTORE report with request_id values), use the
Partner diagnostic.
How it signs
The tester builds the canonical string and signs it exactly as documented:
X-Bridge-Signature = base64( HMAC_SHA256( secret, METHOD\nPATH\nSHA256_HEX(body)\nTIMESTAMP\nNONCE ) )
It sends X-Bridge-Client-Id, X-Bridge-Timestamp, X-Bridge-Nonce, and
X-Bridge-Signature on every call (plus an Idempotency-Key on writes). If a
real call returns 401, work through the canonical recipe on the
signing vectors page — sign the exact bytes you send,
keep the timestamp within ±5 minutes, and use a fresh nonce per request.
Partner diagnostic — v1 Phase 1 ✓ Live
Step-by-step troubleshooting for integration issues. Run it from your own network with your credentials; it records connection errors, HTTP status, latency, and
X-Request-Idper call so OccaStore support can look up the exact server-side trace.
Download (both files — same folder):
partner-diagnostic.mjs— the diagnostic runnerbridge-api-tester.mjs— signing helper (required dependency of the diagnostic script)
Pure Node ≥ 20, no npm install. For a lighter sign-and-send tool see Terminal tester.
When to use this
- Create order (
POST /v1/orders) returns an unexpected status - Intermittent
401/403/5xxand you need a reproducible trace - You want to confirm stock, catalogue, and pricing paths before attempting a write
- OccaStore support asked for
request_idvalues from failed calls
The default run is read-only (no order created). Add --create only when you
intend to place a real order on the environment you target (staging uses the
live fulfilment-system tenant).
Configure
All configuration is via environment variables. The secret is never printed.
| Variable | Purpose |
|---|---|
BRIDGE_BASE_URL |
Gateway base URL — see Environments (default https://dev-api.occastore.com; use https://api.occastore.com with production credentials after go-live) |
BRIDGE_CLIENT_ID |
Your issued Client ID — identifies your account in the report |
BRIDGE_SECRET |
Your issued HMAC secret |
BRIDGE_SKU |
SKU to probe on read paths and on --create (default EXAMPLE-SKU) |
BRIDGE_PRICE |
unit_price_pence for --create (default 1000) |
BRIDGE_REFERENCE |
Order reference for --create (auto-generated if unset) |
BRIDGE_LABEL |
Optional friendly name in the report (e.g. your company name) |
Run
Place both .mjs files in the same directory, then:
Read-only diagnostic (safe — no order):
node partner-diagnostic.mjs
Save a JSON report (send this file or its contents to support):
node partner-diagnostic.mjs --json bridge-diag-report.json
Include create-order (real write — supervised):
node partner-diagnostic.mjs --create --json bridge-diag-create.json
Offline signing only (no credentials, no network):
node partner-diagnostic.mjs --self-test
Exit codes: 0 all critical steps passed · 1 a step failed · 2 misconfiguration.
What it checks
| Step | Call | Pass signal |
|---|---|---|
| Signing | offline vectors | Local HMAC matches signing vectors |
| Health | GET /status |
status: ok, version + DB/Redis |
| Auth enforce | bad signature → GET /v1/stock/:sku |
401 on invalid signature |
| Stock | GET /v1/stock/:sku |
200 or documented 404 |
| Catalogue | GET /v1/catalogue/:sku |
200 (required before create) |
| Pricing | GET /v1/pricing/:sku |
200 or documented 404 |
| Create | POST /v1/orders |
Only with --create; expect 201 |
Reporting results to OccaStore
The script prints a SHARE WITH OCCASTORE block at the end. Copy that section into your support ticket. Include:
client_id(your Client ID)base_urlyou tested against- Every
request_idfrom failed steps
OccaStore correlates request_id with server request_log and fulfilment-system
audit rows.
Common create-order failures
Switch on the reason field in the JSON body. Full list:
Error codes.
| HTTP | reason |
Usual cause |
|---|---|---|
| 401 | bad_signature |
Wrong secret, body bytes changed after signing, clock skew |
| 401 | unknown_client |
Wrong Client ID |
| 400 | missing_idempotency_key |
Idempotency-Key header missing on POST |
| 403 | scope_disabled |
orders.write not granted |
| 403 | account_suspended |
Account disabled |
| 404 | sku_not_found |
SKU not in your catalogue cache |
| 422 | insufficient_stock |
Quantity exceeds available |
| 409 | reference_conflict |
reference already used |
| 503 | channel_not_ready |
Sales channel not yet verified for your account |
For 401 issues, work through Authentication troubleshooting
first.
Cloudflare / scripted clients
Automated HTTP clients hitting the public hostname may receive a Cloudflare challenge page (HTML instead of JSON). The diagnostic flags this explicitly. If you see it, retry from an allowed network or ask OccaStore to run the same diagnostic from the server-side loopback URL.
Postman collection — v1 Phase 1 ✓ Live
Import this collection and every request is automatically signed for you — a collection-level pre-request script builds the canonical string and computes the
X-Bridge-Signatureon every send. Set three variables, hit Send, done.
Download: occastore-bridge.postman_collection.json
The pre-request script uses the exact canonical recipe from the signing vectors and reference signers, so a request that sends from Postman will sign identically to your own client.
Import & go
- In Postman: Import → drop in the downloaded
occastore-bridge.postman_collection.json. - Open the collection → Variables tab and set:
baseUrl— the gateway base URL — see Environments:https://dev-api.occastore.com— staging / Design Preview integrationhttps://api.occastore.com— production (after go-live; use production credentials)
bridgeClientId— your issued Client IDbridgeSecret— your issued HMAC secret (stored as a secret-type variable; never leaves Postman, never committed)
- Open any request and click Send. The pre-request script signs it
automatically — you do not touch the
X-Bridge-*headers.
The sku and bridgeOrderId variables fill in the path placeholders for the
stock/catalogue/pricing reads and the get/cancel-order calls.
What the pre-request script does
On every send it:
- resolves
{{variables}}in the path and body (so it signs the exact bytes Postman will send), - computes
bodyHash = SHA-256(body)(hex; empty body →e3b0c4…b855), - builds the canonical string
METHOD\nPATH\nbodyHash\nTIMESTAMP\nNONCEwith a fresh unix-seconds timestamp and a fresh UUID nonce, - sets
X-Bridge-Signature = base64( HMAC_SHA256( bridgeSecret, canonical ) ),
then the request's headers pull X-Bridge-Client-Id, X-Bridge-Timestamp,
X-Bridge-Nonce, and X-Bridge-Signature from the variables it just set. Writes
also send a fresh Idempotency-Key. If a call returns 401, re-check the
signing vectors: the timestamp must be within ±5 minutes and
the nonce must be unused (10-minute replay window).
The signing logic in this collection is verified in CI against the published signing vectors (
tests/unit/postman-collection.test.js) — the Postman signer cannot drift from the gateway.
Error code reference — v1
Every error response shares one flat shape (never a nested object):
{ "error": "<http-class word>", "reason": "<stable_snake_code>" }
Some errors add a sibling field (field, conflicting_order_id). Switch on
reason — it is the stable contract identifier; error is just the
HTTP-status word and is not guaranteed consistent across surfaces for the same
status. Every response carries an X-Request-Id header (quote it in support
requests). The fulfilment system's internals are never echoed in error bodies.
Money is always integer pence. The upstream_* reasons
(upstream_rejected / upstream_unavailable / upstream_circuit_open /
upstream_error) all refer to the fulfilment system — the backend Bridge
writes your orders to.
This reference covers every error the live (Phase 1 ✓ Live) endpoints can return. Not every endpoint can return every code — the per-endpoint docs list the subset each one emits. The Remediation column is your next action; see Retry guidance below for the transient-vs-terminal rule.
Authentication — 401 (see authentication.md)
The seven HMAC signing stages, in the order they are checked. All are
{ "error": "unauthorized", "reason": … }:
| HTTP | reason |
When | Remediation |
|---|---|---|---|
| 401 | missing_headers |
One or more X-Bridge-* headers absent. |
Send all four X-Bridge-* headers (and Idempotency-Key on writes). |
| 401 | timestamp_skew |
Timestamp malformed, or outside the ±5-minute window. | Send current unix-seconds; sync your clock (NTP). |
| 401 | nonce_replay |
Nonce reused within the 10-minute replay window. | Generate a fresh UUID nonce for every request. |
| 401 | unknown_client |
Client ID not recognised. | Check X-Bridge-Client-Id; confirm your account is provisioned. |
| 401 | revoked_credential |
Credential revoked. | Obtain a new credential from your account contact. |
| 401 | expired_credential |
Credential past its expiry. | Rotate to a current credential. |
| 401 | bad_signature |
Signature does not match the canonical string. | Re-check the canonical recipe (signing vectors); sign the exact bytes you send. |
Account status — 403 / 401
Checked after the signature is valid:
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 403 | forbidden |
account_suspended |
Account suspended, or onboarding not yet at go-live. | Contact your account manager; do not retry until resolved. |
| 401 | unauthorized |
account_revoked |
Account revoked. | Contact your account manager. Terminal. |
| 403 | forbidden |
account_pending |
Account provisioned but not yet activated. | Wait for activation confirmation, then retry. |
Authorization (scope) — 403 / 503
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 403 | forbidden |
scope_disabled |
The required scope is not granted/enabled for your account. | Request the scope from your account contact; do not retry until granted. |
| 503 | service_unavailable |
scope_check_unavailable |
The scope lookup failed and the gateway fails closed. | Transient — back off and retry with the same Idempotency-Key. |
Request validation — 400
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 400 | bad_request |
invalid_request |
Bad partner input — one reason across all v1 endpoints. On order endpoints: malformed/unknown/missing body field (field names the offending path; unknown top-level / address / item fields are rejected — strict, no field smuggling), or a non-UUID path id. On read endpoints (/v1/stock|catalogue|pricing/:sku): the :sku path segment fails the allowed pattern ^[A-Za-z0-9_-]{1,64}$ (response carries field: "sku"). |
Fix the named field (or the :sku) and resend. Terminal — do not retry unchanged. |
| 400 | bad_request |
missing_idempotency_key |
A write endpoint was called without the Idempotency-Key header. |
Add an Idempotency-Key (a fresh UUID) and resend. |
| 400 | bad_request |
idempotency_key_conflict |
The supplied Idempotency-Key is already in use by a different account. |
Use a UUID you generated yourself; resend. |
Conflict — 409
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 409 | conflict |
reference_conflict |
The order reference has already been used for your account (body adds conflicting_order_id). One reference = one order, forever, per customer. |
Use a new unique reference, or treat conflicting_order_id as the existing order. Do not retry the same reference. |
| 409 | conflict |
idempotency_conflict |
The same Idempotency-Key was reused with a different request body. |
Use a fresh Idempotency-Key for the new body. Terminal. |
Business rules — 422
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 422 | unprocessable |
invalid_sku |
A SKU is not in your catalogue. | Remove/correct the SKU (the offending SKU follows the token); resend. |
| 422 | unprocessable |
order_item_unmapped |
One or more line SKUs are in your catalogue but cannot be mapped to a stock item in OccaStore's fulfilment system. The order was not created; the offending SKU(s) are named after the token (order_item_unmapped:<sku>). |
Contact your account contact to map the named SKU(s); do not retry until mapped. |
| 422 | unprocessable |
invalid_country |
The ship_to.country could not be resolved to a supported country. The order was not created. |
Send an ISO-3166 alpha-2 code (e.g. GB); resend. |
| 422 | unprocessable |
reference_too_long |
The order reference exceeds the 60-character limit. The order was not created. |
Shorten reference to ≤ 60 chars; resend. |
| 422 | unprocessable |
reference_invalid |
The order reference contains characters outside the allowed set [A-Za-z0-9._-]. The order was not created. |
Strip disallowed characters; resend. |
| 422 | unprocessable |
insufficient_stock |
Strict stock check enabled and stock is short (a cache miss is soft-allowed). | Reduce quantity or wait for restock; do not retry the same quantity. |
| 422 | unprocessable |
no_shipping_service_configured |
The requested shipping service could not be resolved and you have no default. | Send a valid shipping mapping, or ask your account contact to set a default. |
| 422 | unprocessable |
shipping_service_id_unresolved |
The shipping service on the order isn't mapped on your account yet. The order was not created; the service name follows the token (shipping_service_id_unresolved:<name>). |
Send the X-Request-Id to your account contact; do not retry until resolved. |
| 422 | unprocessable |
invalid_order_payload |
The order couldn't be set up — usually a missing account setting (for example, no stock location enabled). The order was not created. | Send the X-Request-Id to your account contact; do not retry until resolved. |
| 422 | unprocessable |
price_unavailable |
A line item has neither a supplied price nor a contract price (price_unavailable:<sku> on the wire). Body may include summary, sku. |
Send unit_price_pence on the line, or have a contract price configured. |
| 422 | unprocessable |
price_variance |
Price check Hold is enabled and a sent unit_price_pence breaches tolerance vs the configured reference (price_variance:<sku> on the wire). Body includes summary (plain English), sku, sent_pence, expected_pence. |
Read summary — contract price does not match what you sent. Correct the line price or contact your account contact. |
| 422 | unprocessable |
order_dispatched |
Cancel attempted on an order already dispatched/delivered. |
Too late to cancel — handle via returns. Terminal. |
| 422 | unprocessable |
order_not_yet_synced |
Cancel attempted before the order reached the fulfilment system. | Retry the cancel shortly (reconciliation in progress). |
| 422 | unprocessable |
order_not_cancellable |
Order is in an unexpected, non-cancellable state. | Inspect the order; contact support with the X-Request-Id if unexpected. |
| 422 | unprocessable |
upstream_rejected |
The fulfilment system rejected the operation (no upstream detail echoed). | Terminal for this request — review the order/cancel; contact support with the X-Request-Id. |
Some
422reasons forPOST /v1/orderscarry an inline detail suffix on the wire (e.g.invalid_sku:<sku>,price_unavailable:<sku>,price_variance:<sku>,order_item_unmapped:<sku>,shipping_service_id_unresolved:<name>). Match on the stable token before the:.
price_variance/price_unavailabledetail fields (additive, v0.1.307+): the body may also includesummary(human-readable),sku, and forprice_variancealsosent_penceandexpected_pence(integer pence). Example:{ "error": "unprocessable", "reason": "price_variance:WPI-CHOC-2KG", "sku": "WPI-CHOC-2KG", "sent_pence": 5499, "expected_pence": 4999, "summary": "Contract price does not match for WPI-CHOC-2KG: you sent £54.99 but the contract price is £49.99." }
Not found — 404
All are { "error": "not_found", "reason": … }. Existence is never leaked: a
resource that belongs to another account is indistinguishable from one that
does not exist.
| HTTP | reason |
When | Remediation |
|---|---|---|---|
| 404 | not_found |
No such order for your account (order get/cancel). | Check the bridge_order_id; it must be one your account created. |
| 404 | sku_not_found |
Single-SKU stock/catalogue lookup: the SKU is not visible to you or has no data. | Confirm the SKU is in your enabled catalogue/locations. |
| 404 | sku_not_priced |
Single-SKU pricing lookup: no price is available for the SKU. | Have a price configured for the SKU; or send price on the order line. |
| 404 | stock_locations_unconfigured |
Your account has no enabled stock locations configured. | Ask your account contact to enable a stock location. |
| 404 | catalogue_categories_unconfigured |
Your account has no enabled catalogue categories configured. | Ask your account contact to enable a catalogue category. |
The bulk list endpoints (
GET /v1/stock,/v1/catalogue,/v1/pricing) do not returnsku_not_found/sku_not_priced— an empty or filtered result is a normal200withitems: [].
Rate limiting — 429
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 429 | too_many_requests |
rate_limited |
Per-(account, scope) quota exceeded. |
Honour Retry-After, then retry with the same Idempotency-Key on writes. See Rate limits. |
Service & upstream — 502 / 503
| HTTP | error |
reason |
When | Remediation |
|---|---|---|---|---|
| 503 | service_unavailable |
channel_not_ready |
Your channel is not yet verified. | Wait for channel verification; do not retry until verified. |
| 503 | service_unavailable |
upstream_circuit_open |
Fulfilment-system circuit breaker open. | Honour Retry-After: 60, then retry with the same Idempotency-Key. |
| 502 | bad_gateway |
upstream_unavailable |
The fulfilment system failed after the retry policy. On POST /v1/orders the order may or may not have reached the fulfilment system. |
Retry the same Idempotency-Key (deduped) — never a new key. See below. |
| 503 | service_unavailable |
upstream_error |
An unexpected internal error. | Transient — retry with the same Idempotency-Key. |
| 503 | service_unavailable |
writes_paused |
Bridge globally paused for writes by the operator (planned maintenance / incident). Read endpoints continue to serve. | Honour Retry-After: 60; retry the write with the same Idempotency-Key. |
| 503 | service_unavailable |
draining |
Bridge draining for restart/maintenance — new requests rejected, in-flight allowed to complete. | Honour Retry-After: 60, then retry with the same Idempotency-Key. |
| 503 | service_unavailable |
kill_switch_check_unavailable |
Internal kill-switch state read failed; the gateway fails closed. | Transient — retry with the same Idempotency-Key. |
Retry guidance
- Transient — retry (
429/502/503): back off and retry with the sameIdempotency-Key(safe; no duplicate side effect). Honour anyRetry-After. - Terminal — do not retry unchanged (
400/409/422, and an authentication/credential401): fix the request, signing, or credential first. Retrying the identical request will return the identical error. - The
502 upstream_unavailablerule (writes). OnPOST /v1/ordersa502means the order may already exist in the fulfilment system. Retry the SAMEIdempotency-Key— never issue a new key, never retry blind. Same key + same body within the dedup window replays the original outcome (returning the already-created order rather than creating a duplicate). A new key would create a second order. The same rule applies to a502/503on cancel.
Internal / defence-in-depth codes
These exist in the gateway but are not part of the normal partner contract — a correctly-authenticated integration should not see them in routine operation. Listed for completeness:
| HTTP | error |
reason |
Note |
|---|---|---|---|
| 500 | server_error |
idempotency_storage_failed |
Idempotency store read failed (infrastructure fault). Rare; retry. |
| 401 | unauthorized |
missing_auth_context |
Defence-in-depth guard behind authentication; not reachable with a valid signature. |
| 503 | service_unavailable |
kill_switch_active |
Defensive path for a mis-configured route switch; not partner-triggerable in normal config. |
| 401 | unauthorized |
admin_auth_required |
Admin-only diagnostics surface; not part of the partner API. |
Conventions — v1 Phase 1 ✓ Live
The cross-cutting rules that apply across every /v1 endpoint, documented
once here. The endpoint pages link back to this page rather than restating them.
Money — integer pence
All monetary amounts are integer pence (e.g. unit_price_pence). Never send
or expect a decimal, a float, or a currency symbol: £49.99 is 4999. This avoids
floating-point rounding error end to end.
Timestamps — ISO 8601, UTC
Response timestamps (e.g. as_of) are ISO 8601 in UTC (e.g.
2026-06-07T10:30:00Z). Treat them as UTC.
Don't confuse this with the
X-Bridge-Timestampauth header, which is unix seconds (not ISO) and must be within ±5 minutes — see Authentication.
Country — ISO 3166-1 alpha-2
ship_to.country must be an ISO 3166-1 alpha-2 code — two letters, e.g. GB,
IE, FR. A value that cannot be resolved is rejected with
422 invalid_country (see Error codes).
Order reference
Your per-order reference must be:
- ≤ 60 characters (longer →
422 reference_too_long); - composed only of the characters
[A-Za-z0-9._-]— letters, digits, dot, underscore, hyphen (anything else →422 reference_invalid); - unique per account, forever — one
referencemaps to exactly one order; a reuse returns409 reference_conflictwith the existingconflicting_order_id.
Idempotency — Idempotency-Key
Every write (POST /v1/orders, cancel) requires an Idempotency-Key
header (a fresh UUID you generate):
- Same key + same body within the dedup window replays the original outcome — it returns the order you already created rather than creating a second one. This is your retry-safety handle.
- Same key + a different body →
409 idempotency_conflict. - A key already used by a different account →
400 idempotency_key_conflict. - On a
502 upstream_unavailable, retry the SAME key — never a new key: the order may already exist, and the same key is deduped. A new key would create a duplicate. See Error codes → Retry guidance and Rate limits.
Nonce & replay
X-Bridge-Nonce is a fresh UUID per request; a nonce is rejected if reused
within the 10-minute replay window. Full signing rules:
Authentication.
Rate limits — v1 Phase 1 ✓ Live
Every authenticated /v1 request passes three independent rate limiters,
AND-composed — a request must satisfy all of them, and any one rejection
returns 429. Limits are enforced per (account, scope): your stock.read
traffic and your orders.write traffic are metered separately.
The default limits
| Limiter | Default | Window | Purpose |
|---|---|---|---|
| Burst | 20 requests | 1 second | short-spike cap |
| Per-minute | 60 requests | 60 seconds | sustained rate |
| Per-day | 10 000 requests | 24 hours | daily ceiling |
These are the out-of-the-box defaults. Your account may be provisioned with higher (or lower) limits — the response headers below always tell you the limit actually in force, so read them rather than hard-coding the defaults.
Response headers
Every rate-limited response (success and 429) carries, sourced from the
most-constrained limiter at that moment:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The limit (points) of the tightest applicable limiter. |
X-RateLimit-Remaining |
Requests remaining in the current window (0 on a 429). |
X-RateLimit-Reset |
Unix-seconds time when that window refills. |
Retry-After |
(429 only) seconds to wait before retrying. |
The 429 response
{ "error": "too_many_requests", "reason": "rate_limited" }
Honour Retry-After: back off for that many seconds, then retry. A 429 is
transient — the same request will succeed once the window refills, so retry
with the same Idempotency-Key on writes (it is deduped; see
Error codes → Retry guidance).
Practical guidance
- Spread your requests. The burst limiter allows a short spike (20/s) but the per-minute limiter (60/min) is the sustained ceiling — a steady ~1 req/s is comfortably within both.
- Reads and writes count separately (per scope), so a burst of stock reads does not consume your order-creation budget.
- Trust the headers, not the table. If your account has custom quotas, the
numbers above are not your numbers —
X-RateLimit-Limitis authoritative.
Availability note (honest): rate limiting is DoS protection and fails open — if the limiter's backing store is briefly unavailable, requests are allowed through and the
X-RateLimit-*headers are omitted for those responses. Authentication and authorization never fail open; only this layer does. So an occasional response with noX-RateLimit-*headers is expected and benign.
Versioning & deprecation — v1 Phase 1 ✓ Live
How the OccaStore Bridge contract evolves, and what you can rely on.
The version in the path
The major version is in the URL: /v1. A breaking change to the v1
contract after General Availability (GA) would ship under a new major (/v2) — a
v1 integration keeps working unchanged. The current status banner is
"v1 Design Preview" (see below).
Design Preview vs GA
Until Phase 7 GA, these docs are labelled "v1 Design Preview" and are
noindex. During the preview window the contract may still change — including in
breaking ways — as the surface is finalised with early integrators. Every
such change is announced in the changelog. Once v1 reaches
GA, the preview reservation ends and the compatibility promise below takes full
effect.
What counts as a breaking vs an additive change
Additive (safe; may ship any time, announced in the changelog):
- a new endpoint, a new optional request field, a new response field;
- a new
reasonvalue for an existing error class; - a new webhook event type (when webhooks ship).
Write your client to ignore unknown fields and to treat an unrecognised
reasonas its HTTP status class. Then additive changes never break you.
Breaking (post-GA → new major; pre-GA → announced in the changelog):
- removing or renaming a request/response field;
- removing or renaming an error
reasontoken; - changing an endpoint path, method, or success status code;
- tightening validation in a way that rejects previously-accepted input.
Deprecation policy (post-GA)
When a contract surface is deprecated after GA, we will: (1) announce it in the changelog with the replacement and a removal date; (2) keep the old surface working through the stated notice period; and (3) where practical, emit both old and new in parallel during that window. The notice period is set per change and stated in the changelog entry.
Worked example — the upstream_* error rename (Design Preview)
The first applied example of this policy is a pre-GA breaking change made
under the Design Preview reservation: two 422 error reason tokens that named
an internal backend were retired in favour of vendor-neutral tokens —
- the order-rejection reason (a
422on create/cancel) is nowupstream_rejected; - the cancel-before-propagation reason (a
422on cancel) is noworder_not_yet_synced.
This was a clean cut (no alias) because the change shipped during Design Preview, before any integration was live on those paths — exactly the window the preview banner reserves. It is recorded in the changelog. After GA, the same change would instead carry a deprecation notice period.
Backlog (logged, not yet decided): the response field
pk_order_idmay be renamed to a vendor-neutral name (e.g.fulfilment_order_id) before GA. It is the highest-blast-radius rename (it appears on every successful order response), so it is a deliberate, separately-announced pre-GA decision — not folded into the error-token rename above.
GET /v1/catalogue — product catalogue Phase 1 ✓ Live
Returns the catalogue record for a SKU that is visible to your account, served from Bridge's cache of your product catalogue. Visibility is gated by your enabled stock locations and catalogue categories.
- Scope:
catalogue.read - Auth: HMAC (see authentication.md)
- Idempotency: n/a (read)
- Kill switches: reads honour
drain_mode(503 draining); not affected by the writes pause.
There are two forms: a single-SKU lookup and a paginated bulk list.
GET /v1/catalogue/:sku — single SKU
- Path param:
sku— must match^[A-Za-z0-9_-]{1,64}$.
Request
GET /v1/catalogue/WPI-CHOC-2KG HTTP/1.1
Host: dev-api.occastore.com
# + the four X-Bridge-* auth headers (see Authentication)
Response — 200 OK
X-Cache: hit|miss header + body:
{
"sku": "WPI-CHOC-2KG",
"title": "Whey Protein Isolate — Chocolate 2kg",
"barcode": "5012345678900",
"category_id": "b2c3d4e5-6f70-4a81-9b2c-3d4e5f60718a",
"retail_price_gbp": 54.99,
"weight_kg": 2.05,
"dimensions_cm": { "height": 24, "width": 16, "depth": 16 },
"tax_rate": 20,
"images": [
{ "source": "https://cdn.example/wpi-choc-2kg-thumb.jpg", "full_source": "https://cdn.example/wpi-choc-2kg.jpg", "is_main": true, "sort_order": 0 }
],
"attributes": { "flavour": "chocolate" },
"stock_item_id": "a1b2c3d4-...",
"as_of": "2026-05-19T14:00:00.000Z"
}
| Field | Type | Notes |
|---|---|---|
sku |
string | Echo of the requested SKU. |
title |
string | null | Product title. |
barcode |
string | null | Primary barcode. |
category_id |
string | null | Opaque category identifier (UUID) for the SKU — match it exactly; do not parse. null when the SKU has no category. |
retail_price_gbp |
number | null | Retail reference price in GBP. This is not your contract price — use pricing.md (price_pence) for the price you are charged. |
weight_kg |
number | null | Unit weight in kilograms. |
dimensions_cm |
object | { height, width, depth } in centimetres; each value a number or null. |
tax_rate |
number | null | VAT rate (percent). |
images |
array | Objects { source, full_source, is_main, sort_order }; [] when none. |
attributes |
object | Map of attribute name → value, filtered to the attributes enabled for your account. {} when none are enabled. |
stock_item_id |
string | null | Stock-item reference for the SKU; null where not yet set. |
as_of |
string (ISO-8601) | Timestamp of the cached record. |
Most fields derive from your product catalogue and may be
nullwhere not populated in your data.
GET /v1/catalogue — bulk list
Paginated. Query params and the response envelope follow the shared
Read endpoints & pagination contract in the API overview. Each
items[] element is the same object as the single-SKU response above.
{
"items": [ { "sku": "WPI-CHOC-2KG", "title": "Whey Protein Isolate — Chocolate 2kg", "barcode": "5012345678900", "category_id": "b2c3d4e5-6f70-4a81-9b2c-3d4e5f60718a", "retail_price_gbp": 54.99, "weight_kg": 2.05, "dimensions_cm": { "height": 24, "width": 16, "depth": 16 }, "tax_rate": 20, "images": [], "attributes": { "flavour": "chocolate" }, "stock_item_id": "a1b2c3d4-...", "as_of": "2026-05-19T14:00:00.000Z" } ],
"page": 1,
"per_page": 100,
"total_pages": 5,
"total_items": 432,
"has_more": true
}
SKUs not visible to your account (location/category not enabled) are silently
omitted — the bulk endpoint never returns sku_not_found. An empty result is a
normal 200 with items: [].
Errors
| HTTP | reason |
Meaning |
|---|---|---|
| 400 | invalid_request |
:sku does not match ^[A-Za-z0-9_-]{1,64}$ (single-SKU only); response carries field: "sku". |
| 401 | unauthorized |
HMAC / credential failure — see authentication.md. |
| 403 | scope_disabled |
catalogue.read not granted. |
| 404 | stock_locations_unconfigured |
Your account has no enabled stock locations. |
| 404 | catalogue_categories_unconfigured |
Your account has no enabled catalogue categories. |
| 404 | sku_not_found |
The SKU is not visible to you or has no cached record (single-SKU only). |
| 429 | rate_limited |
Quota exceeded. |
| 503 | draining |
Bridge draining for restart/maintenance — Retry-After: 60. |
| 503 | upstream_error |
Unexpected internal error — transient, retry. |
See error-codes.md for the full reference.
GET /v1/stock — stock levels Phase 1 ✓ Live
Returns cached, eventually-consistent stock levels aggregated across your
enabled stock locations. Bridge serves these from a cache the prefetch worker
refreshes — see refresh_cadence_seconds in the response for the cadence.
- Scope:
stock.read - Auth: HMAC (see authentication.md)
- Idempotency: n/a (read)
- Kill switches: reads honour
drain_mode(503 draining); they are not affected by the writes pause.
There are two forms: a single-SKU lookup and a paginated bulk list.
GET /v1/stock/:sku — single SKU
- Path param:
sku— must match^[A-Za-z0-9_-]{1,64}$.
Request
GET /v1/stock/WPI-CHOC-2KG HTTP/1.1
Host: dev-api.occastore.com
# + the four X-Bridge-* auth headers (see Authentication)
Response — 200 OK
X-Cache: hit|miss header + body:
{
"sku": "WPI-CHOC-2KG",
"available": 142,
"reserved": 8,
"on_order": 50,
"due_in": null,
"earliest_expiry": null,
"as_of": "2026-05-19T14:00:00.000Z",
"refresh_cadence_seconds": 300
}
| Field | Type | Notes |
|---|---|---|
sku |
string | Echo of the requested SKU. |
available |
integer | Sum of available units across your enabled locations. |
reserved |
integer | Sum of reserved units. |
on_order |
integer | Sum of on-order units. |
due_in |
null | Reserved — always null in v1. Not yet populated; treat as nullable, do not depend on a value. |
earliest_expiry |
string (YYYY-MM-DD) | null | Earliest batch expiry across your enabled locations, from the stock-prefetch cache (Query Data script 8 EarliestExpiry and/or Linnworks batch inventory). null when the SKU is not batch-tracked or expiry is not yet in cache — run stock-prefetch (and ensure catalogue cache has stock_item_id for batch lookup). |
as_of |
string (ISO-8601) | Timestamp of the most recent per-location data in the aggregate. |
refresh_cadence_seconds |
integer | How often the underlying cache is refreshed (300). |
GET /v1/stock — bulk list
Paginated. Query params and the response envelope follow the shared
Read endpoints & pagination contract in the API overview.
Each items[] element is the same object as the single-SKU response above.
{
"items": [ { "sku": "WPI-CHOC-2KG", "available": 142, "reserved": 8, "on_order": 50, "due_in": null, "earliest_expiry": null, "as_of": "2026-05-19T14:00:00.000Z", "refresh_cadence_seconds": 300 } ],
"page": 1,
"per_page": 100,
"total_pages": 3,
"total_items": 247,
"has_more": true
}
An empty or fully-paged result is a normal 200 with items: [] — the bulk
endpoint never returns sku_not_found.
Errors
| HTTP | reason |
Meaning |
|---|---|---|
| 400 | invalid_request |
:sku does not match ^[A-Za-z0-9_-]{1,64}$ (single-SKU only); response carries field: "sku". |
| 401 | unauthorized |
HMAC / credential failure — see authentication.md. |
| 403 | scope_disabled |
stock.read not granted. |
| 404 | stock_locations_unconfigured |
Your account has no enabled stock locations. |
| 404 | sku_not_found |
The SKU has no cached data for your locations (single-SKU only). |
| 429 | rate_limited |
Quota exceeded. |
| 503 | draining |
Bridge draining for restart/maintenance — Retry-After: 60. |
| 503 | upstream_error |
Unexpected internal error — transient, retry. |
See error-codes.md for the full reference.
GET /v1/pricing — your prices Phase 1 ✓ Live
Returns the price you are charged for a SKU — a per-SKU override if one is
configured for your account, otherwise your channel baseline. There is no
retail-price fallback: a SKU with no override and no channel price returns
404 sku_not_priced.
- Scope:
pricing.read - Auth: HMAC (see authentication.md)
- Idempotency: n/a (read)
- Kill switches: reads honour
drain_mode(503 draining); not affected by the writes pause.
There are two forms: a single-SKU lookup and a paginated bulk list.
GET /v1/pricing/:sku — single SKU
- Path param:
sku— must match^[A-Za-z0-9_-]{1,64}$.
Request
GET /v1/pricing/WPI-CHOC-2KG HTTP/1.1
Host: dev-api.occastore.com
# + the four X-Bridge-* auth headers (see Authentication)
Response — 200 OK
X-Cache: hit|miss header + body:
{
"sku": "WPI-CHOC-2KG",
"price_pence": 4999,
"currency": "GBP",
"source": "channel",
"as_of": "2026-05-19T14:00:00.000Z",
"refresh_cadence_seconds": 3600
}
| Field | Type | Notes |
|---|---|---|
sku |
string | Echo of the requested SKU. |
price_pence |
integer | Your unit price in integer pence (never a float). |
currency |
string | Always "GBP" in v1. |
source |
string | Where the price came from — "override" (a per-SKU override on your account) or "channel" (your channel baseline). These are the only two values. |
as_of |
string (ISO-8601) | Timestamp of the price. |
refresh_cadence_seconds |
integer | How often the underlying channel price cache is refreshed (3600). |
GET /v1/pricing — bulk list
Paginated. Query params and the response envelope follow the shared
Read endpoints & pagination contract in the API overview. Each
items[] element is the same object as the single-SKU response above (an
override, where present, takes precedence over the channel price).
{
"items": [ { "sku": "WPI-CHOC-2KG", "price_pence": 4999, "currency": "GBP", "source": "channel", "as_of": "2026-05-19T14:00:00.000Z", "refresh_cadence_seconds": 3600 } ],
"page": 1,
"per_page": 100,
"total_pages": 4,
"total_items": 318,
"has_more": true
}
SKUs not visible or not priced for your account are silently omitted — the bulk
endpoint never returns sku_not_priced. An empty result is a normal 200 with
items: [].
Errors
| HTTP | reason |
Meaning |
|---|---|---|
| 400 | invalid_request |
:sku does not match ^[A-Za-z0-9_-]{1,64}$ (single-SKU only); response carries field: "sku". |
| 401 | unauthorized |
HMAC / credential failure — see authentication.md. |
| 403 | scope_disabled |
pricing.read not granted. |
| 404 | stock_locations_unconfigured |
Your account has no enabled stock locations. |
| 404 | catalogue_categories_unconfigured |
Your account has no enabled catalogue categories. |
| 404 | sku_not_priced |
No override and no channel price for the SKU (single-SKU only). |
| 429 | rate_limited |
Quota exceeded. |
| 503 | draining |
Bridge draining for restart/maintenance — Retry-After: 60. |
| 503 | upstream_error |
Unexpected internal error — transient, retry. |
See error-codes.md for the full reference.
Order lifecycle — v1 Phase 1 ✓ Live
The status you see on GET /v1/orders/:bridge_order_id is
Bridge's mirror of the order's state in the fulfilment system, kept
eventually-consistent by the reconciliation worker. This page is the state model
behind that field and behind the cancel tokens.
Status values
These are the only status values an order can hold:
| Status | Meaning | Phase 1 |
|---|---|---|
accepted |
Recorded in Bridge and accepted for fulfilment — the initial state of every order. | ✓ emitted |
processing |
Being picked / packed in the fulfilment system. | not emitted yet |
dispatched |
Left the warehouse. | ✓ emitted |
delivered |
Delivered to the customer. | not emitted yet |
cancelled |
Cancelled — terminal. | ✓ emitted |
Phase 1 coverage (known limitation). Automatic updates currently resolve
accepted → dispatched → cancelledonly.processinganddeliveredare valid states but not emitted yet: the fulfilment system's open API exposes no pick/pack sub-state (soprocessingcan't be distinguished fromaccepted) and delivery is carrier-tracking (a Phase 3 surface). Treatdispatchedas "left the warehouse"; don't wait forprocessingordeliveredto appear.
The lifecycle
create
│
▼
accepted ─────► processing ─────► dispatched ─────► delivered
│ │ │ │
│ cancel │ cancel └──────┬──────────┘
└────────┬───────┘ │
▼ too late to cancel
cancelled (422 order_dispatched)
(terminal — a repeat cancel
is an idempotent 200)
An order is cancellable while it is accepted or processing. Once it is
dispatched (or delivered) it is too late to cancel — handle that via returns.
cancelled is terminal: cancelling an already-cancelled order returns an
idempotent 200.
Cancellation windows
What a cancel attempt returns, mapped onto the state above (see Cancel order and Error codes):
| Cancel result | Order state | What to do |
|---|---|---|
200 (status: cancelled) |
was accepted / processing |
Done — it's cancelled. |
422 order_not_yet_synced |
accepted, but not yet propagated to the fulfilment system (its pk_order_id is still null) |
Retry the cancel shortly — reconciliation is in progress. |
422 order_dispatched |
dispatched / delivered |
Too late to cancel; handle via returns. |
422 order_not_cancellable |
an unexpected, non-cancellable state | Inspect; contact support quoting the X-Request-Id. |
POST /v1/orders — create an order Phase 1 ✓ Live
Creates an order in the fulfilment system (Source = EDI, your SubSource) and
returns the Bridge order record. The cross-cutting rules — integer pence,
reference format, ISO country codes, and Idempotency-Key semantics — are in
Conventions.
- Scope:
orders.write - Auth: HMAC (see README)
Idempotency-Keyheader: required- Content-Type:
application/json
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
reference |
string | ✓ | Your PO / order reference — ≤60 chars, [A-Za-z0-9._-] only (else 422 reference_too_long / reference_invalid). One reference = one order, forever, per customer (reuse → 409 reference_conflict). |
customer_reference |
string | — | Your end-customer's reference (free text). |
items |
array | ✓ | ≥1 item. |
items[].sku |
string | ✓ | Must exist in your catalogue. |
items[].quantity |
integer | ✓ | ≥1. |
items[].unit_price_pence |
integer | — | Integer pence. Omit → your contract price is used. If sent and it drifts from contract, it is accepted (warn-logged) — your price is authoritative — unless your account has price check → Hold configured (see Price check below). Neither sent nor contract → 422 price_unavailable. |
ship_to |
object | ✓ | name, address1, town, postcode, country required; company, address2, address3, region, phone, email optional. |
bill_to |
object | — | Same shape as ship_to; defaults to ship_to. Ignored when your account has a centrally managed billing address configured (operator sets this in Bridge admin). |
shipping_service |
string | — | A configured bridge_service_name; omitted → your default. Unresolvable → 422 no_shipping_service_configured. |
requested_dispatch_date |
string (ISO 8601) | — | Defaults to now + 2 days (spec §4[13]). |
Unknown top-level / address / item fields are rejected (400 invalid_request, strict no-smuggling).
Request
POST /v1/orders HTTP/1.1
Host: dev-api.occastore.com
Content-Type: application/json
Idempotency-Key: 7c9f0e2a-3b1d-4a6e-9f8c-1a2b3c4d5e6f
# + the four X-Bridge-* auth headers (see Authentication)
{
"reference": "PO-EXAMPLE-001",
"customer_reference": "WEB-EXAMPLE-44821",
"items": [{ "sku": "WPI-CHOC-2KG", "quantity": 2, "unit_price_pence": 4999 }],
"ship_to": {
"name": "Jane Example", "company": "Example Trading Ltd",
"address1": "1 Example Way", "town": "Leeds",
"postcode": "LS1 1AA", "country": "GB", "email": "jane@example.test"
},
"shipping_service": "next_day_dpd"
}
Response — 201 Created
Bridge-Order-Id header + body:
{
"bridge_order_id": "3f1a8c2e-9d44-4e21-8a77-0c2e3f1a8c2e",
"reference": "PO-EXAMPLE-001",
"status": "accepted",
"pk_order_id": "123456",
"items": [{ "sku": "WPI-CHOC-2KG", "quantity": 2, "unit_price_pence": 4999 }],
"shipping_service": "next_day_dpd",
"created_at": "2026-05-19T14:00:00.000Z"
}
Same Idempotency-Key + same body within 24h replays this exact 201.
Errors (this endpoint)
| HTTP | reason |
Meaning |
|---|---|---|
| 400 | invalid_request |
Body schema / unknown field / missing required (field names it). |
| 400 | missing_idempotency_key |
Idempotency-Key header absent. |
| 400 | idempotency_key_conflict |
The Idempotency-Key is already in use by a different account. |
| 401 | unauthorized |
HMAC / credential failure — see authentication.md for the seven granular reasons. |
| 403 | scope_disabled |
orders.write not granted. |
| 403 | account_suspended |
Customer not active (suspended, or onboarding not yet go-live). |
| 409 | reference_conflict |
reference already used (body includes conflicting_order_id). |
| 409 | idempotency_conflict |
Same key, different body. |
| 422 | invalid_sku |
A SKU is not in your catalogue. |
| 422 | order_item_unmapped |
A line SKU is in your catalogue but not mapped to a stock item in OccaStore's fulfilment system. The order was not created; the offending SKU(s) are named (order_item_unmapped:<sku>). |
| 422 | invalid_country |
ship_to.country could not be resolved to a supported country (use an ISO-3166 alpha-2 code, e.g. GB). The order was not created. |
| 422 | reference_too_long |
reference exceeds 60 characters. The order was not created. |
| 422 | reference_invalid |
reference contains characters outside [A-Za-z0-9._-]. The order was not created. |
| 422 | insufficient_stock |
Strict stock check on + short (cache miss is soft-allowed). |
| 422 | no_shipping_service_configured |
Shipping service unresolvable (none requested and no default). |
| 422 | shipping_service_id_unresolved |
The order's shipping service isn't mapped on your account yet. The order was not created; the service name follows the token (shipping_service_id_unresolved:<name>). Send the X-Request-Id to your account contact. |
| 422 | invalid_order_payload |
The order couldn't be set up — usually a missing account setting. The order was not created. Send the X-Request-Id to your account contact. |
| 422 | price_unavailable |
No sent price and no contract price. Response may include summary and sku. |
| 422 | price_variance |
Price check Hold is enabled and a sent unit_price_pence breaches tolerance against the configured reference price (price_variance:<sku> on the wire). Response includes summary, sku, sent_pence, expected_pence. |
| 422 | upstream_rejected |
The fulfilment system rejected the order (no upstream internals echoed). |
| 429 | rate_limited |
Quota exceeded (Retry-After). |
| 502 | upstream_unavailable |
The fulfilment system failed after retries — idempotency key stays valid, safe to retry. |
| 503 | channel_not_ready |
Channel not yet verified for this customer. |
| 503 | upstream_circuit_open |
Fulfilment-system circuit open — Retry-After: 60. |
| 503 | writes_paused |
Bridge globally paused for writes — Retry-After: 60. |
| 503 | draining |
Bridge draining for restart/maintenance — Retry-After: 60. |
| 503 | upstream_error |
Unexpected internal error — transient, safe to retry with the same key. |
Price check (operator-configured)
Most accounts have no price check — contract drift on sent prices is warn-logged and the order is accepted (your sent price is authoritative).
When your account contact enables price check → Hold, Bridge compares each
line's sent unit_price_pence against a reference price (contract or
tradeit, within a configured tolerance). On breach the order is not
created:
| HTTP | reason (wire) |
Meaning |
|---|---|---|
| 422 | price_variance:<sku> |
Sent price outside tolerance vs the reference. Body includes summary (e.g. Contract price does not match…), sent_pence, expected_pence. |
| 422 | price_unavailable:<sku> |
Reference price could not be resolved for that SKU. Body may include summary. |
Example price_variance body:
{
"error": "unprocessable",
"reason": "price_variance:WPI-CHOC-2KG",
"sku": "WPI-CHOC-2KG",
"sent_pence": 5499,
"expected_pence": 4999,
"summary": "Contract price does not match for WPI-CHOC-2KG: you sent £54.99 but the contract price is £49.99."
}
Match on the stable token before the : (same pattern as other suffixed 422
reasons). Warn mode never rejects — breaches are operator-logged only.
Static billing (operator-configured)
When a centrally managed billing address is configured for your account, Bridge
stamps that address on the fulfilment order and ignores any bill_to you
send. ship_to is unchanged.
See error-codes.md. For a guided external test run with
request_id capture, use the Partner diagnostic.
GET /v1/orders/:bridge_order_id — order detail Phase 1 ✓ Live
Returns one order from the Bridge mirror (Bridge's authoritative record of what you were told the order contained; kept eventually-consistent with the fulfilment system by the reconciliation worker). Amounts are integer pence and timestamps are ISO 8601 UTC — see Conventions.
- Scope:
orders.read - Auth: HMAC (see README)
- Idempotency: n/a (read)
- Path param:
bridge_order_id— the UUID returned byPOST /v1/orders(also theBridge-Order-Idresponse header).
Request
GET /v1/orders/3f1a8c2e-9d44-4e21-8a77-0c2e3f1a8c2e HTTP/1.1
Host: dev-api.occastore.com
# + the four X-Bridge-* auth headers (see Authentication)
Response — 200 OK
X-Cache: hit|miss header + body:
{
"bridge_order_id": "3f1a8c2e-9d44-4e21-8a77-0c2e3f1a8c2e",
"reference": "PO-EXAMPLE-001",
"status": "accepted",
"pk_order_id": "123456",
"items": [{ "sku": "WPI-CHOC-2KG", "quantity": 2, "unit_price_pence": 4999 }],
"created_at": "2026-05-19T14:00:00.000Z",
"updated_at": "2026-05-19T14:00:00.000Z"
}
status∈accepted·processing·dispatched·delivered·cancelled. Cached 30s foraccepted/processing, 1h for terminal states (a status change may take up to ~30s to surface).pk_order_idisnull(not omitted) while an order is in the reconstruction-pending window.
Phase 1 status coverage (known limitation). In Phase 1, automatic status updates resolve to
accepted→dispatched→cancelledonly.processinganddeliveredare not emitted yet: the fulfilment system's order API exposes no pick/pack sub-state to distinguishprocessingfromaccepted, and delivery confirmation is carrier-tracking (a Phase 3 surface). Treatdispatchedas "left the warehouse"; do not wait forprocessingordeliveredto appear in Phase 1. The full enum is reserved so values can be added without a contract change as later phases land.
Errors (this endpoint)
| HTTP | reason |
Meaning |
|---|---|---|
| 400 | invalid_request |
bridge_order_id is not a UUID. |
| 401 | unauthorized |
HMAC / credential failure — see authentication.md. |
| 403 | scope_disabled |
orders.read not granted. |
| 404 | not_found |
No such order for your account. Identical response whether the id does not exist or belongs to another customer — existence is never leaked. |
| 429 | rate_limited |
Quota exceeded. |
| 503 | draining |
Bridge draining for restart/maintenance — Retry-After: 60. (Reads honour drain_mode; they are not affected by the writes pause.) |
| 503 | upstream_error |
Unexpected internal error — transient, retry. |
See error-codes.md.
POST /v1/orders/:bridge_order_id/cancel — cancel an order Phase 1 ✓ Live
Cancels an order in the fulfilment system and marks the Bridge record
cancelled. "Cancel" is state-targeting: an already-cancelled order returns
200 idempotently (no second fulfilment-system call). Idempotency-Key
semantics are shared across writes — see Conventions.
- Scope:
orders.cancel(distinct fromorders.write— granted separately) - Auth: HMAC (see README)
Idempotency-Keyheader: required- Path param:
bridge_order_id— the UUID fromPOST /v1/orders. - Request body: none required.
Request
POST /v1/orders/3f1a8c2e-9d44-4e21-8a77-0c2e3f1a8c2e/cancel HTTP/1.1
Host: dev-api.occastore.com
Idempotency-Key: 9a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
# + the four X-Bridge-* auth headers (see Authentication)
No request body.
Response — 200 OK
{
"bridge_order_id": "3f1a8c2e-9d44-4e21-8a77-0c2e3f1a8c2e",
"reference": "PO-EXAMPLE-001",
"status": "cancelled",
"pk_order_id": "123456",
"items": [{ "sku": "WPI-CHOC-2KG", "quantity": 2, "unit_price_pence": 4999 }]
}
Behaviour by current order state:
| State | Result |
|---|---|
accepted / processing |
Cancelled in the fulfilment system → 200, status:"cancelled". |
cancelled |
Idempotent 200 (no fulfilment-system call). |
dispatched / delivered |
422 order_dispatched — too late to cancel. |
accepted with pk_order_id still null |
422 order_not_yet_synced — retry shortly (reconciliation in progress). |
A fulfilment-system rejection (e.g. the order was already dispatched there)
surfaces as 422 upstream_rejected — the fulfilment system itself is the final
authority on whether a cancel is permitted.
Errors (this endpoint)
| HTTP | reason |
Meaning |
|---|---|---|
| 400 | invalid_request |
bridge_order_id not a UUID. |
| 400 | missing_idempotency_key |
Header absent. |
| 400 | idempotency_key_conflict |
The Idempotency-Key is already in use by a different account. |
| 401 | unauthorized |
HMAC / credential failure — see authentication.md. |
| 403 | scope_disabled |
orders.cancel not granted. |
| 404 | not_found |
No such order for your account (no existence leak). |
| 409 | idempotency_conflict |
Same key, different body. |
| 422 | order_dispatched |
Order already dispatched/delivered. |
| 422 | order_not_yet_synced |
Not yet in the fulfilment system; retry shortly. |
| 422 | order_not_cancellable |
Order in an unexpected non-cancellable state. |
| 422 | upstream_rejected |
The fulfilment system rejected the cancel. |
| 502 | upstream_unavailable |
The fulfilment system failed after retries — idempotency key stays valid, safe to retry. |
| 503 | upstream_circuit_open |
Fulfilment-system circuit open — Retry-After: 60. |
| 503 | writes_paused |
Bridge globally paused for writes — Retry-After: 60. |
| 503 | draining |
Bridge draining for restart/maintenance — Retry-After: 60. |
| 503 | upstream_error |
Unexpected internal error — transient, safe to retry with the same key. |
See error-codes.md.
Sandbox — v1 Design preview
There is no live sandbox environment yet. This page describes what a sandbox will offer and is published as a design preview so you can plan. No credentials, no sandbox endpoint, and no
SANDBOXtraffic are accepted today. Do not point an integration at a "sandbox" URL — there isn't one.
What you can do today, with no sandbox
You can validate the entire signing path offline, right now, before you ever send a live request — that is the hardest part of integrating, and it needs no sandbox:
- Signing vectors — reproducible worked examples; match them byte-for-byte to prove your signer is correct.
- Reference signers — copy-paste signers in Node, Python, PHP, and C#, each reproducing those vectors.
- Terminal tester — a zero-dependency script whose
--checkruns an offline signing self-test (no credentials, no network). - Postman collection — import it and the pre-request script signs every request for you.
Between them you can confirm your signing, headers, and request shapes are correct without any live or sandbox call.
What a sandbox will add (planned, not committed to a date)
A future sandbox is expected to offer an isolated environment where you can
exercise the live request/response flow — create and cancel test orders, see
real status transitions, and hit the real error paths — against isolated test
data, without touching production fulfilment. The SANDBOX SubSource is
reserved for this and is not assignable today.
A live sandbox environment and its credentialing flow are a separate backend project. When it ships, its full contract (base URL, how to get sandbox credentials, data-isolation guarantees) will be published here — and this page's banner will flip from "Design preview" to a live phase pill, the same way the live endpoints were documented only once they shipped.
Until then: treat anything "sandbox" as not available. Build and validate with the offline tooling above; go live against the documented production contract when your account is provisioned.
Webhooks — v1 Phase 3
Design preview — webhooks are not live yet. There is no endpoint to register, and no deliveries are sent today. This page describes the intended model so you can design for it; do not build an integration that depends on webhooks until this page's pill flips from Phase 3 to a live phase.
What webhooks will do
Push order lifecycle events to an endpoint you control, as they happen — so you don't have to poll. Planned event types map onto the Order lifecycle:
order.dispatched— the order left the warehouse (status: dispatched);order.cancelled— the order was cancelled (status: cancelled);order.tracking— carrier tracking detail, once available (a later Phase 3 surface).
The intended model
- You register an HTTPS endpoint you own (e.g.
https://your-app.example.com/webhooks). - Bridge POSTs a signed JSON event to it on each lifecycle change.
- Each delivery is HMAC-signed with the same scheme as the API, so you verify it with the exact signing vectors / reference signers you already use — that's how you confirm a delivery genuinely came from us.
- At-least-once delivery with retries; each event carries a stable id so you can dedupe.
The full contract (registration, the event schema, the signature header, and the retry policy) will be published here when webhooks ship — the same way every live endpoint was documented only once it shipped.
What to do today (poll instead)
Until webhooks ship, poll
GET /v1/orders/:bridge_order_id and compare the status
against the Order lifecycle. The reconciliation worker keeps
the Bridge mirror current, so polling reflects the real transitions
(accepted → dispatched → cancelled).
Register interest
If push delivery matters to your integration, tell your account manager — it helps us prioritise the Phase 3 work. There is nothing to configure yet.
Roadmap
These surfaces are planned for future phases and are not yet available. Do not integrate against them — there is no contract here to build on. Each one's full contract is published in these docs only when it ships, the same way the live endpoints above are documented.
Webhooks Phase 3
Push notifications for order lifecycle events (status changes, dispatch, delivery), HMAC-signed. See the Webhooks design preview for the intended model and what to do today.
Order tracking Phase 3
Carrier tracking detail per order, available after dispatch.
Orders list Phase 3
Paginated list and query of your orders.
OAuth2 Phase 4
OAuth2 client-credentials authentication as an alternative to HMAC.
Changelog — v1 Phase 1 ✓ Live
Generated build: build v0.1.405 · bcc1b9a · 2026-08-16T17:43:31.490Z
The contract-version changelog for the OccaStore Bridge v1 API. Newest
first, one entry per change to the partner-facing surface (endpoint paths,
request/response fields, error reasons, status enums, SubSource rules, webhook
payloads) plus the developer tooling published alongside it. Build-stamp,
styling, and internal-only changes are not listed.
While v1 is in Design Preview (see versioning) entries are
dated under v1; formal v1.x minor tags begin at GA.
v1 — Design Preview
2026-07-30
- Added — Environments: staging (
dev-api) vs production (api) base URLs, per-environment credentials, and tooling variable guidance. - Revised — Integration types, README, Authentication: production gateway is deployed; go-live uses new credentials issued by your account manager (staging creds do not work on production).
2026-06-30
- Added — Create order + Error codes:
422price_variance:<sku>andprice_unavailable:<sku>responses onPOST /v1/ordersnow include additive fieldssummary(plain English),sku, and for price variance alsosent_penceandexpected_pence. The stablereasontoken is unchanged — match before the:as before.
2026-06-29
- Added (operator-configured) — Create order: accounts may
have a centrally managed billing address (partner
bill_toignored when set) and an optional price check on lines with an explicitunit_price_pence. Default behaviour is unchanged (contract drift is warn-logged and accepted). When price check Hold is enabled, a breach returns422with reasonprice_variance:<sku>(orprice_unavailable:<sku>when the reference price is missing). Documented in Error codes.
2026-06-26
- Documented — Create order + Error codes:
two
422responses you may see onPOST /v1/orders—shipping_service_id_unresolved(the order's shipping service isn't mapped on your account yet) andinvalid_order_payload(the order couldn't be set up, usually a missing account setting). Both are configuration items on our side rather than a problem with your request, and the order is not created — quote the responseX-Request-Idto your account contact and we'll resolve it. - Clarified — Catalogue:
category_idis an opaque identifier (UUID) — match it exactly rather than parsing it — andstock_item_idmay benullwhere it isn't set (typestring | null).
2026-06-19
- Added — Partner diagnostic: step-by-step external
troubleshooting script with per-call
X-Request-Idcapture and a shareable support report. Downloads:partner-diagnostic.mjs+bridge-api-tester.mjs. - Revised — Authentication troubleshooting and Terminal tester: cross-links to the partner diagnostic.
2026-06-17
- Revised — Integration types: align EDI/API (staging
dev-api,GET /status, prod not deployed) and drop-ship (per-partner go-live, review/auto, periodic stock + despatch-after-fulfilment timing) with gateway and DS worker behaviour. - Documented — Authentication: short conformance window
exception for
pendingaccounts during onboarding tests. - Revised — README:
GET /statusin endpoint table; rate limits per(account, scope). Postman + terminal-tester examples use staging URL for Design Preview. - Revised — Integration types: drop-ship SFTP section trimmed to customer-facing scope only (file-exchange overview, integration pack as the authoritative spec, setup and test checklist). Operator config detail (cadence keys, dialect inheritance, buffers, billing hooks) removed from the public docs surface.
- Added — Integration types: branching guide for the
two live partner paths — EDI/API (signed
/v1HTTPS; links the Quickstart and endpoint reference) and drop-ship SFTP (high-level CSV file exchange). Overview and Quickstart now link here.
2026-06-07
- Documented — zero-downtime credential rotation: an account may hold multiple active credentials at once (issue new → migrate your signer → revoke old). See Authentication troubleshooting.
- Added (design preview) — Webhooks: a Phase 3 design preview
of push delivery for order lifecycle events (HMAC-signed with the same scheme).
Not live — no endpoint to register yet; poll
GET /v1/orders/:idtoday. - Added — Authentication troubleshooting: the
401checklist (sign the transmitted bytes, stale secret, clock skew, nonce replay, path-excludes-query, header/field order) with the diagnostic tools. - Added — Order lifecycle: the
statusstate model (accepted/processing/dispatched/delivered/cancelled) with the cancellation windows and the cancel-token mapping. - Added — Conventions: the single-source page for the
cross-cutting rules (integer pence, ISO 8601 UTC dates, ISO 3166 country codes,
referenceformat,Idempotency-Keysemantics); endpoint pages now link to it. - Added — Quickstart: a golden-path onboarding guide (credentials → sign → first authenticated GET → create a test order → cancel), each step linking the relevant endpoint and tooling.
- Breaking (pre-GA clean cut) — two
422errorreasontokens that named the backend (on the create-order and cancel paths) were retired in favour of vendor-neutral tokens: the order-rejection reason is nowupstream_rejectedand the cancel-before-propagation reason is noworder_not_yet_synced. The public contract (docs + error bodies) no longer names the fulfilment system. Done under the Design-Preview reservation, before any integration was live on these paths; the first worked example of the versioning & deprecation policy. - Added — Postman collection: an import-and-go collection whose collection-level pre-request script auto-signs every request.
- Added — Terminal tester: a zero-dependency
bridge-api-tester.mjswith--check/--create/--canceland an offline signing self-test. - Added — Reference signers: copy-paste HMAC signers in Node, Python, PHP, and C#.
- Added — Signing vectors: reproducible HMAC-SHA256 worked examples for validating your signer offline.
2026-06-06
- Added (error
reasons) — four422 unprocessablevalidation reasons onPOST /v1/orders:reference_too_long,reference_invalid,order_item_unmapped, andinvalid_country. See error codes.
This page is regenerated on every docs build; the build stamp above is the authoritative record of which version/commit you are reading.