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.

  • Reviewed
  • 11 min read
  • API version 2026-04-01

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.

Key prefixes, where they belong and what they authorise
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.
Never on the client

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.

request and response headers
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-Version to 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

  1. Read the changelog entries between your pin and the target. They are additive far more often than not.
  2. In test mode, send the new version as a header on your highest-traffic endpoint and run your suite.
  3. Repeat for webhook handling: the event body follows the version pinned on the endpoint, not on the request that caused it.
  4. 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.

the three outcomes
# 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" }
How a repeated key is resolved
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.
The rule that matters

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.

Buckets and steady-state ceilings
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 handling
# 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.

If a key has leaked

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.

Codes this layer returns and the correct response
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.