Authentication, API versions and idempotency
Four headers, two key types and one dated version string decide whether a request works — and whether retrying it charges a customer twice. This page is the contract underneath every other guide.
Key types and scopes
Every account holds two key pairs, one per mode. The prefix carries both the mode and the privilege level, so a key is self-describing in a log line or a stack trace.
| Prefix | Side | Scope | On exposure |
|---|---|---|---|
| pk_test_ · pk_live_ | Browser | Mount hosted fields, tokenise a card, read the method list for a country. | Publishable by design. It appears in your page source and that is correct. |
| sk_test_ · sk_live_ | Server | Full read and write on every resource in that mode. | Treat as a breach. Rotate immediately — see rotation. |
| whsec_ | Server | Not an API credential. Verifies inbound webhook signatures only. | Rotate the endpoint secret; deliveries keep working through the overlap window. |
A secret key in front-end JavaScript, a mobile binary or a mobile app's network trace is a full compromise: it can create payouts as well as charges. If a build has ever shipped one, rotate before you patch — the old binary is still in someone's cache.
An authenticated request
Authentication is a bearer token. There is no signing step, no nonce and no OAuth dance for your own account's data — TLS plus a secret key is the whole scheme.
GET https://api.bazpay.com/v1/payments/pay_3fJ2Qk8vTnAuthorization: Bearer sk_test_9f2c… # requiredBazPay-Version: 2026-04-01 # optional, overrides the pinIdempotency-Key: ord_10482 # required on writesContent-Type: application/json # on request bodies# every response carries these backBazPay-Request-Id: req_2mQ7xF9pLkBazPay-Version: 2026-04-01 TLS 1.2 or higher is required. A plain HTTP request is refused outright rather than redirected, so a misconfigured client fails loudly instead of leaking a key over the wire.
BazPay-Request-Id is the single most useful thing to log. It identifies the exact
exchange on our side, so a support question that includes it can be answered from the trace
rather than from a reproduction.
Choosing an API version
Versions are dates, not integers. Your account is pinned to the version that was current when it was created, and it stays there until you move it deliberately. Nothing we release can change the behaviour of a pinned version.
- Account pin Set in the dashboard. Applies to every request that does not say otherwise, including calls from the SDKs and the CMS plugins.
- Per-request header Send
BazPay-Versionto override the pin for one call. This is how you test a newer version on one endpoint without moving the account. - Response echo Every response states the version it was served under. Assert on it in your integration tests and a silent pin change cannot go unnoticed.
- Deprecation A dated version stays callable for at least 24 months after its successor ships. Removal is announced by email and in the changelog, never by a response that simply stops working.
Moving to a newer version safely
- Read the changelog entries between your pin and the target. They are additive far more often than not.
- In test mode, send the new version as a header on your highest-traffic endpoint and run your suite.
- Repeat for webhook handling: the event body follows the version pinned on the endpoint, not on the request that caused it.
- Move the account pin. Keep the header override in place for one release so a rollback is a config change, not a deploy.
Idempotency
Every write accepts an Idempotency-Key, and every write should carry one. The key
is yours to choose; the useful choice is something already unique in your system, such as an
order id plus an attempt counter.
# first call — the charge happensPOST /v1/payments Idempotency-Key: ord_10482→ 201 { "id": "pay_3fJ2Qk8vTn", "status": "requires_action" }# network died, client retries the identical requestPOST /v1/payments Idempotency-Key: ord_10482→ 200 { "id": "pay_3fJ2Qk8vTn", "status": "requires_action" } BazPay-Idempotent-Replay: true# same key, different body — refused, nothing chargedPOST /v1/payments Idempotency-Key: ord_10482 amount: 9900→ 409 { "code": "idempotency_conflict" } | Situation | Result |
|---|---|
| Same key, same body, first call finished | The stored response is replayed with BazPay-Idempotent-Replay: true. No second charge. |
| Same key, same body, first call still running | 409 idempotency_in_progress. Wait and retry; do not switch keys. |
| Same key, different body | 409 idempotency_conflict. Nothing is created. Use a fresh key for a genuinely different request. |
| Key older than 24 hours | The record has expired. The request is treated as new and will execute again. |
A timeout is not a failure. If a write times out, the safe response is to resend the identical request with the identical key — never to give up and never to generate a new key, which is precisely how the same basket gets charged twice.
Rate limits and back-off
Limits are per account and per bucket, measured over a rolling second. They are set well above normal traffic; if you are meeting them steadily, the shape of the integration is usually the problem rather than the ceiling.
| Bucket | Live | Test | Notes |
|---|---|---|---|
| Writes — payments, refunds, payouts | 100/s | 25/s | Short bursts to 2× are absorbed before the limiter engages. |
| Reads — single object | 250/s | 50/s | Use expand instead of a second read. |
| Lists and reports | 20/s | 10/s | Page with cursors; a report export does not count against this. |
| Webhook replays | 10/s | 10/s | Bulk replay from the dashboard is queued, not rate limited. |
# 429 response headersBazPay-RateLimit-Limit: 100BazPay-RateLimit-Remaining: 0BazPay-RateLimit-Reset: 1776072731Retry-After: 4# correct client behaviour, in wordssleep Retry-After seconds, plus jitter of 0-1sresend with the SAME Idempotency-Keydouble the wait on each further 429, cap at 60sgive up after 6 attempts and alert
Two habits keep you far from the ceiling. Do not poll a payment's status — subscribe to
the event instead, which is both faster and free. And use the
expand parameter to inline related objects, so one call replaces the three reads
that a naive client would make.
Rotation and leaks
Keys are rotatable without downtime because both the old and the new key are valid during an overlap you control. The routine below takes a few minutes and is worth rehearsing in test mode before you need it in anger.
Create the replacement
Generate a second secret key in the dashboard. Both keys now authorise every call, so there is no cut-over moment and no window where requests fail.
Roll it through your fleet
Deploy the new value to configuration and restart. Watch the per-key request counter in the dashboard until the old key is at zero — that counter, not your deploy tool, is what proves nothing is still using it.
Revoke the old key
Revocation is immediate and final. Any request still carrying the old key gets 401 key_invalid from that instant, which is exactly what you want if the reason for rotating was a leak.
Reverse the order: revoke first, then repair. A short outage costs less than an attacker with payout rights. Tell us at once — we can freeze payout creation on the account inside a few minutes while you work, and the audit log will show every call the key made.
Errors from this layer
Authentication, versioning and idempotency produce a small, stable set of codes. Branch on the
code; the human message is for your logs and may be reworded.
| Status | Code | Cause | Do this |
|---|---|---|---|
| 401 | key_missing | No Authorization header. | Fix the client. Never retried automatically. |
| 401 | key_invalid | Key is wrong, revoked, or from the other mode. | Check the prefix against the environment before rotating. |
| 403 | key_scope | A publishable key was used for a server call. | Move the call server-side; do not promote the key. |
| 400 | version_unknown | The header is not a released version date. | Compare against the changelog on the documentation home. |
| 409 | idempotency_conflict | A key was reused with a different body. | Use a fresh key. Nothing was created. |
| 429 | rate_limited | Bucket exhausted. | Honour Retry-After, keep the same idempotency key. |
| 5xx | gateway_error | Our side failed to complete. | Retry with the same key. Writes are safe to repeat. |
Payment-specific decline codes are a different list and live in accepting a payment. The complete field-level index is in the API reference.
Keep reading
Stuck on this page?
Integration questions reach an engineer, not a queue. Send the request id and the response body — that is usually enough to answer in one reply.