# Authentication

One credential model for every surface. Bearer tokens, scoped access, and an environment baked into each key.

Every request authenticates with a **bearer token** in the `Authorization` header:

```bash title="Authorization header"
-H "Authorization: Bearer sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

The token's prefix tells you — and Payer — exactly what it is, who holds it, and which environment it belongs to. Tokens are shown **once** at issuance, stored only as a hash, and can be rotated or revoked without downtime. Every issue, rotation and revocation lands in an audit ledger.

## Credential types

| Prefix | Kind | Who holds it | What it can do |
| --- | --- | --- | --- |
| `pk_live_` · `pk_test_` | Publishable key | A merchant, client-side | Create checkout sessions only. Safe to ship in a browser or app bundle. |
| `sk_live_` · `sk_test_` | Secret key | A merchant, server-side | Full merchant scope, bounded by the scopes you grant. Never expose it. |
| `pat_` | [Personal access token](/developers/docs/personal-tokens) | An individual, on their own account | Read your own balance, transactions and insights. |
| `oat_` | [OAuth2 access token](/developers/docs/oauth) | A third-party app, for a user | Whatever the user consented to, and nothing more. |
| `whsec_` | [Webhook signing secret](/developers/docs/webhooks) | Your endpoint | Verify that a delivery really came from Payer. |

> [!WARNING]
> A secret key (`sk_`) can move money and read everything its scopes allow. Keep it server-side, in a secret manager — never in a repo, a browser bundle, or a mobile app. If one leaks, rotate it from the dashboard immediately.

## Scopes

Access is capability-based. A scope means the same thing whoever holds the credential, and a token can only reach what its scopes admit — every other field is refused, even other reads.

| Scope | Grants | Held by |
| --- | --- | --- |
| `checkout:write` | Create and cancel checkout sessions | `pk`, `sk` |
| `balances:read` | Read wallet balances | `sk`, `pat`, `oat` |
| `transactions:read` | Read transaction history and insights | `sk`, `pat`, `oat` |
| `wallets:read` | Read wallet details | `sk`, `pat`, `oat` |
| `kyc:read` | Read KYC status | `sk`, `pat`, `oat` |
| `payments:write` | Move money | `sk` (step-up required) |
| `payouts:write` · `refunds:write` | Payouts and refunds | `sk` |
| `webhooks:manage` | Manage webhook endpoints | `sk` |

Grant a credential the least it needs. A key that only creates checkout sessions can't read a balance, and a request for a scope the key doesn't hold gets a `403` — see [Errors](/developers/docs/errors).

## Environments

The sandbox is a **separate deployment** with its own accounts, keys and data, running the same code path as production — so what you test is what ships. You never point at a different host to reach it.

| Environment | Key prefix | Base URL |
| --- | --- | --- |
| Sandbox | `sk_test_` · `pk_test_` | `https://api.payer.app` |
| Production | `sk_live_` · `pk_live_` | `https://api.payer.app` |

Same URL, same request shape — swap the key to move between them. A test key can't touch production data, and a live key can't touch the sandbox. The environment is carried on the credential, so it also shows up in logs and in the dashboard.

## Rate limits

Limits are **per credential**, so one busy integration can't throttle the rest of your account. When you exceed a limit you get a `429`.

| Credential | Limit |
| --- | --- |
| Publishable key (`pk_`) | 60 requests / minute |
| Secret key (`sk_`) | 600 requests / minute |
| Personal access token (`pat_`) | 300 requests / minute |
| OAuth2 access token (`oat_`) | 300 requests / minute |
| OAuth token endpoint (`/oauth/token`) | 60 requests / minute, per client |

## Idempotency

Every mutating request is safe to retry. Send an `Idempotency-Key` header (any unique string — a UUID or your own order id) on a `POST`, and a repeat of the same key replays the original response instead of acting twice:

```bash title="An idempotent write"
-H "Idempotency-Key: 5f4d3c2b-1a09-4e8d-b7c6-5a4b3c2d1e0f"
```

- The **same key with the same body** replays the stored response, tagged `Idempotent-Replay: true`.
- The **same key with a different body** is rejected with a `422`.
- A key is scoped to the credential that used it, and remembered for 24 hours.
