# Scopes and least privilege

Every API key holds an explicit list of scopes. A request that needs a scope the key does not have is rejected with `403`, before anything happens.

The point is not to make your life harder. It is that **a leaked key should be limited by construction**, not by how fast you notice.

## The scopes

| Scope | Grants |
| --- | --- |
| `payments:read` | Read payments and their status |
| `payments:write` | Create payments (debit a customer) |
| `links:read` | Read payment links |
| `links:write` | Create, update, deactivate and delete payment links |
| `invoices:read` | Read invoices |
| `invoices:write` | Create, publish and cancel invoices |
| `invoices:send` | **Send** an invoice to a recipient |
| `files:write` | Request an upload URL |

## Why `invoices:send` is separate

Every other write scope acts on your own data. Sending puts *your brand* in front of *an arbitrary recipient you name*, with a link to pay.

That is the phishing-shaped capability in this API, and it is the one worth withholding by default. A key that creates invoices for your billing run does not need to email anyone; keeping the two apart means a leak of that key cannot be turned into a campaign that looks like it came from you.

## Choosing scopes

Start from what the consumer does, not from what it might do later.

**A checkout server** takes money and reads back status:

```
payments:write, payments:read
```

That is the recommended default, and it is worth noticing what it *cannot* do: create links, touch invoices, or send anything. A stolen checkout key can only collect money into your account.

**A reconciliation job** should never be able to move money:

```
payments:read, invoices:read, links:read
```

**A billing service** that raises invoices but leaves delivery to a human:

```
invoices:write, invoices:read, files:write
```

Add `invoices:send` only when the same service is genuinely doing the sending.

## The cost of checking

The check is an array lookup against a key you have already authenticated and loaded. It adds no round trip, no database query and nothing measurable to your latency.

It happens in a guard before the handler runs, so a `403` costs you strictly less than a successful request.

## Things that surprise people

**Scopes are fixed at creation.** To change them, mint a new key and rotate. That is deliberate: a key whose powers can grow silently is not a bound.

**A `403` for scope looks like a `403` for IP allowlisting.** The message distinguishes them. Check the response body before assuming which.

**Sandbox enforces scopes too.** A sandbox that is more permissive than production teaches you the wrong thing and fails on launch day.

## Next steps

- [Environments and API keys](/en/concepts/environments-and-keys) — where a key's power starts
- [Managing API keys](/en/payments/api-keys) — minting and rotating with scopes
- [Errors](/en/concepts/errors) — what a `403` is telling you
