Public API
/api/public — what a third party consumes
The only surface consumed by someone outside the business. Small on purpose, and the one place where a breaking change costs a phone call rather than a deploy.
Important
What exists today is one endpoint group: batch product lookup by EAN or SKU, returning a customer's price and live stock. It is not a general-purpose commerce API, and this page should not be read as promising one.
Who calls it
A third party integrating against the client — a procurement system, a marketplace, a customer's own purchasing tool.
| Authentication | An X-API-Key header, on a scheme of its own |
| Authorization | The key scheme alone — no policy beyond a valid, live key |
| Scope | Bypass; the caller names the customer |
| Rate limit | Yes — a fixed window per token, tunable live |
| Contract stability | Published, external. Assume someone depends on every field |
Tokens
Credentials are created and revoked from the admin surface. Two kinds exist, and the difference decides how the caller names a customer:
| Type | Bound to | The caller supplies |
|---|---|---|
| Integrator | Nothing — it can act for any customer | X-Customer-No on every request |
| Customer | One customer, at creation | Nothing; the customer comes from the token |
The plaintext key is shown once and never stored. Only a hash and a short non-secret prefix persist — the prefix so a token can be identified in a list without revealing it. A lost key cannot be recovered, only replaced.
Tokens carry an optional expiry, a revocation timestamp, and a last-used timestamp. Last-used is the one to look at before revoking something nobody remembers issuing.
What it returns
Batch lookup, by EAN or by SKU, capped at 100 keys per call. Each result carries whether the product was found, the customer's net unit price at quantity one with promotions excluded, the currency, and live stock.
Every successful response is wrapped:
{
"data": [
{ "key": "SHIRT-BLK-M", "found": true, "value": { "unitPrice": 16.33, "currency": "AUD" } }
]
}
Why an envelope on this surface only. An external consumer benefits from one predictable shape
they can write a client against, and from room to add fields beside data later without breaking
anyone. Internal surfaces do not need that — they change together with their callers.
Why unknown keys are not errors. A batch of a hundred SKUs where four are unknown is a normal
result, not a failure. Each item reports found individually, so the caller gets the ninety-six it
asked for.
Failures look different here too
Errors on this surface go through a problem shape of their own, applied at the outermost layer of the pipeline — ahead of everything else. That ordering is deliberate: it guarantees that a fault anywhere inside Commerce cannot leak an internal error shape, a stack trace or a domain-specific message to an external party.
400 |
Missing X-Customer-No on an integrator token, an empty list, or more than 100 keys |
401 |
Missing, invalid, revoked or expired key |
404 |
Unknown customer |
The rate limiter's one subtlety
The limit is partitioned per token, so one noisy integrator cannot exhaust everyone else's budget. Making that work required putting the rate limiter after authorization in the pipeline, which is the opposite of the usual advice.
The reason: the API-key scheme is not the default scheme, so it only runs during authorization. Place the limiter earlier and there is no token identity to partition by yet — every public request falls into one bucket and the isolation silently disappears. It is a comment in the code, repeated here because comments get deleted.
If you are extending this surface
Three things are true here that are not true anywhere else in Commerce:
- Someone outside will notice a change. Additive is safe; renaming and removing are not.
- The envelope is the contract. Return the payload; the wrapper adds
dataaround it. - A rate limit exists and applies. Test with it on, not around it.