Authentication
Every Data Plane call is authenticated with a single header:
X-API-Key: <token>
The token is never stored in clear text — we keep a salted SHA-256 hash — so it is shown to you exactly once, when the key is created in the Dashboard. Lose it and you rotate it.
:::danger Never put the key in a browser The key is a bearer credential with no origin binding. Anything that reaches a browser — a fetch from your frontend, an example pasted into a console — leaks it to every script on the page. Call the API from your backend. :::
What we check, in order
A request that fails any of these stops there:
- The key exists (hash lookup).
- The key is not revoked and not expired.
- The key's scopes cover the endpoint → otherwise
403. - The caller IP is in the key's allowlist, if one is set → otherwise
403. - The rate limit for this key and endpoint → otherwise
429.
Scopes
Scopes are section:action pairs assigned when the key is created:
| Scope | Grants |
|---|---|
product:read | Browse the catalog, look up items and prices |
orders:write | Create orders and checkouts |
funds:read | Read balances and the transaction ledger |
A key only ever gets what you gave it — there is no implicit escalation. Issue
narrow keys per integration: a price-scraper key with product:read cannot
spend money even if it leaks.
IP allowlist
Optional per key. Empty allowlist = no IP restriction.
Supported patterns:
| Form | Example |
|---|---|
| IPv4 single | 198.51.100.42 |
| IPv4 CIDR | 203.0.113.0/24 |
| IPv4 wildcard (legacy) | 10.0.*, 192.168.*.* |
| IPv6 single | 2a02:4780::1 |
| IPv6 CIDR | 2a02:4780:27:1690::/64 |
:::caution No wildcards for IPv6
2a02:4780:* is not accepted. Use CIDR notation.
:::
On mismatch you get HTTP 403 with a message naming the address we saw:
Forbidden: caller IP 2a02:4780::1 is not in this API key's allowlist
We tell you your IP but never echo the allowlist back. The single most common
cause of this error: a bot deployed on a dual-stack host that egresses over
IPv6, while the allowlist only contains the operator's home IPv4 address. Call
GET /whoami to see what we resolve for
you.
Transaction limit
A key can carry a lifetime spend cap (transactionLimit). Before every
purchase we take a quote from the shop service and check:
transaction_used + quote_amount <= transaction_limit
If it does not fit, the order is refused with HTTP 409, statusCode=13
(API_KEY_LIMIT_EXCEEDED) and errorCode="api_key_transaction_limit_exceeded".
The message carries attempted / remaining / limit.
:::warning This is not a temporary condition
Backing off and retrying will fail forever. The cap has to be raised (or
transaction_used reset) from the control plane.
:::
Read transactionRemaining on the key — not status — to know whether a key
can still serve an order of the size you are about to send. A key with
status: active and a small remaining balance will still 409 on any larger
quote.
:::info Breaking change (2026-05)
Exhausting a key's transaction limit used to return 429 LIMIT_REACHED, the
same code as a rate limit. It now returns 409 API_KEY_LIMIT_EXCEEDED.
If your client had special handling for "key limit exhausted" hanging off 429,
move it to 409 + errorCode="api_key_transaction_limit_exceeded", and leave
only back-off logic on 429.
:::
Content-Type enforcement
POST, PUT and PATCH must send Content-Type: application/json. A missing
or different value is rejected with HTTP 415 before validation runs — so a 415
never tells you anything about your payload.