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:
Code
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:
Code
A billing service that raises invoices but leaves delivery to a human:
Code
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 β where a key's power starts
- Managing API keys β minting and rotating with scopes
- Errors β what a
403is telling you

