Authentication & limits
A key is a server credential: no cookie, no session, no CSRF, HTTPS only. Everything on this page is enforced before a controller is reached.
The credential
Authorization: Bearer ak_sbx_4K21PQ7M.0tS3xh_9Yb4Q7pLmE1nRfWc2vJ8dKz6aTgUxHqNsB0
- The key id is the public half — ak_live_… or ak_sbx_… — and identifies the client in every log.
- The secret is 256 random bits, base64url. It is shown once, when the key is issued, and stored only as a peppered hash: nobody at BIR can read it back to you.
- The two are joined by a single dot. Send them in the Authorization header and nowhere else — never a query string, never a form field.
- HTTPS only. A plaintext call in production is refused with TLS_REQUIRED before the key is even looked at.
- No cookie is set and no session is started for /api/ paths, so CSRF does not apply and there is nothing for a browser to replay.
- No CORS headers are sent. The caller is a server; a browser origin calling this API is a misuse, not a configuration problem.
Scopes
A key carries the scopes its agency asked for. A call outside them is refused with SCOPE_MISSING naming the scope it needed — ask for it rather than working around it.
| Scope | Grants |
|---|---|
| verify | Resolve a party and read a compliance position. |
| transact | Create and progress transactions, parties, contractors and contracts. |
| pay | Cite payments and advise collections. |
| documents | Issue and read final documents and their evidence. |
| pv | Raise and read payment vouchers. |
| webhooks | Manage webhook subscriptions. |
| dw | Pull the anonymised data-warehouse extracts. Live keys only. |
A client is also bound to one agency. Every call is filtered by it: another agency's transaction, document or contract reads as not found, because an integrator may not enumerate work that is not theirs. An optional IP allowlist narrows it further — calls from anywhere else are refused IP_NOT_ALLOWED.
Request signing (optional, per client)
A client that handles money can be configured to sign every state-changing call. The signature binds timestamp, method, path and body together, so a captured request cannot be replayed, redirected to another endpoint or altered in flight.
X-Timestamp: 1789123964 X-Signature: v1=<hex hmac_sha256> signing string = ts + "\n" + METHOD + "\n" + path + "\n" + sha256(raw body) signature = "v1=" + hex(hmac_sha256(secret, signing string))
<?php
$ts = time();
$body = json_encode($payload, JSON_UNESCAPED_SLASHES);
$sig = 'v1=' . hash_hmac(
'sha256',
$ts . "\n" . 'POST' . "\n" . '/api/v1/transactions' . "\n" . hash('sha256', $body),
$secret
);- The path is the request path exactly as sent, without the query string.
- The body is the raw bytes you transmit. Sign what you send, not what you meant to send.
- Clock skew beyond ±300 seconds is refused with SIGNATURE_SKEW, and the refusal carries the server's own time so your client can correct itself.
- A timestamp, body and key that have already been seen are refused as a replay.
Rate limits
Limits are per client, not per address, and they apply to reads as well as writes. The default is 120 requests a minute; a daily quota can be set alongside it. Ask for more if your volumes need more — a limit that forces an integrator to spread work across keys is a limit that has stopped measuring anything.
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
HTTP 429 + Retry-After: 60
{ "ok": false, "error": { "code": "RATE_LIMITED", "retryable": true } } Back off on Retry-After. Do not retry in a tight loop: the per-address limiter in front of the key limiter exists to blunt key guessing, and a hammering client is indistinguishable from one.
Idempotency
Every call that can MINT something — a transaction, a party, a document number, a contract, a voucher, a subscription — requires an Idempotency-Key header: up to 64 characters of A–Z a–z 0–9 _ - : ., unique to the operation you are performing. Use your own case reference — it is the one value you can reproduce after a timeout.
A lookup that is a POST only so an identifier stays out of the URL — /parties/verify, /documents/verify, /contracts/lookup — creates nothing a retry could duplicate and takes no key. The endpoint reference marks which is which.
| You send | The platform does |
|---|---|
| A new key | Claims it and processes the call. The claim is an insert against a unique index, so two concurrent retries can never both proceed. |
| The same key, the same body | Replays the stored answer byte for byte, with Idempotent-Replayed: true. Nothing happens twice. |
| The same key, a different body | Refused: IDEMPOTENCY_KEY_REUSED. A key means one intention. |
| The same key while the first is still running | IDEMPOTENCY_IN_PROGRESS with Retry-After. Wait and ask again. |
| The same key more than 24 hours later | IDEMPOTENCY_EXPIRED: the answer was completed and its body has been purged. Fetch the resource instead of retrying. |
| No key at all | IDEMPOTENCY_KEY_REQUIRED. There is no unguarded write path. |
A timeout is not a failure. Retry with the same key: either the first attempt landed and you get its answer, or it did not and the retry does the work.
Rotation and revocation
- Rotating issues a new key and leaves the old one valid for a seven-day overlap. Deploy the new secret, watch your logs, and let the old one expire.
- Revoking takes effect on the next request. There is no cache to wait out and no grace period — that is the point of a revocation.
- Suspending a client stops every key it holds at once, with CLIENT_SUSPENDED.
- If a secret is exposed, say so and have it revoked. A rotated key costs an afternoon; a leaked one that keeps working costs a register.