Every Payments API credential (API key or OAuth client) carries a set of scopes that bound what it can do. Endpoints declare required scopes; a request whose credential lacks them is rejected with 403 before any business logic runs.
Scopes follow the format <action>:<resource>, for example
write:transactions.
All scopes
Transactions
| Scope | Grants |
|---|---|
read:transactions |
View transactions and their status |
write:transactions |
Create and update transactions — no funds move |
move:transactions |
Release escrowed funds forward to the recipient |
reverse:transactions |
Return funds to the sender: refund, cancel, or rescind |
The transaction scopes split on risk, not on which endpoint you call:
read it, mutate it, move money forward, move money back. On v1 the same
write:transactions both created a transaction and released funds from
escrow, so a read-only integration could not be granted create rights without
also being able to move money — v2 separates those.
cancel:transactions and rescind:transactions are the v1 names for the same
operations reverse:transactions covers: returning funds to the sender.
On v2, only reverse:transactions is accepted. All three reversal routes
(POST /transactions/{id}/refund, /cancel and /rescind) require it, and
reverse: does not inherit from the v1 names: the three are siblings, not
parent and child. A token without reverse:transactions is refused on v2
with a 403 even when it carries cancel: or rescind:, so a token minted for
v1 has to be re-issued. Carrying all three is fine, and is what a migrating
client should do.
The v1 names still work on /api/v1, whose routes continue to enforce exactly
them until it sunsets on 1 November 2026. They are no longer offered to
new OAuth clients, so GET /oauth/scopes does not list them; an existing
token that carries them keeps working on v1 regardless.
If you are migrating, add reverse:transactions before you cut over, and
keep the v1 pair until every reversal call is on /api/v2. Dropping them
first breaks the v1 routes, which still enforce exactly those names. A token
may hold all three at once, so there is no window where you have to choose.
Add move:transactions too if you release escrowed funds forward, which is a
separate permission by design.
Customers
| Scope | Grants |
|---|---|
read:customers |
View customer information |
write:customers |
Create and update customers |
Templates
| Scope | Grants |
|---|---|
read:templates |
View payment templates |
write:templates |
Create and update templates |
Disputes
| Scope | Grants |
|---|---|
read:disputes |
View disputes |
write:disputes |
Create and respond to disputes |
submit:evidence |
Submit evidence for disputes |
Webhooks
| Scope | Grants |
|---|---|
read:webhooks |
View webhook configurations |
write:webhooks |
Create and update webhooks |
Compliance
| Scope | Grants |
|---|---|
read:compliance |
View compliance verifications |
write:compliance |
Create compliance verification requests |
Platform
| Scope | Grants |
|---|---|
read:platform |
View own platform information |
write:platform |
Update own platform settings |
manage:api-keys |
Create and revoke API keys |
Admin
| Scope | Grants |
|---|---|
admin:platform |
Full platform access (all scopes) |
Hierarchy rules
You never need to list a read scope alongside its write scope. Write-class scopes automatically include the corresponding read scope:
Note what is not in this table: write:transactions does not grant
move: or reverse:, and neither of those grants the other. Creating a
transaction, releasing funds, and pulling them back are three separate
permissions.
| This scope | Also grants |
|---|---|
write:transactions |
read:transactions |
move:transactions |
read:transactions |
reverse:transactions |
read:transactions |
write:customers |
read:customers |
write:templates |
read:templates |
write:disputes |
read:disputes |
submit:evidence |
read:disputes |
write:webhooks |
read:webhooks |
write:compliance |
read:compliance |
write:platform |
read:platform |
manage:api-keys |
read:platform |
admin:platform grants every scope. It exists for administrative tooling;
do not use it for day-to-day integrations.
Declaring scopes
An OAuth client declares the scopes it may ever request when it is created, on the Workspace API:
curl -X POST https://core-staging.coinbax.com/api/v1/workspaces/$WORKSPACE_ID/oauth/clients \
-H "Authorization: Bearer $WORKSPACE_JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "Payments Integration",
"scopes": ["read:transactions", "write:transactions", "read:customers"],
"grantTypes": ["client_credentials"]
}'
Grant the narrowest set that works. A token request can narrow further with
the optional scope parameter, but it can never widen beyond what the client
declared, so the client’s list is the real ceiling. See
authentication.
When a scope is missing
A request without the required scope returns 403 with the code
INSUFFICIENT_SCOPES. The error message names the scopes the endpoint
requires and the scopes your credential actually has, so the fix is always
explicit:
Insufficient permissions. Required scopes: [write:transactions].
Your key has: [read:transactions, read:customers]
Handle 403 distinctly from 401: a 401 means the credential itself is invalid, a 403 means the credential is valid but under-scoped. See errors and the response envelope.
Practices that hold up
- Least privilege. Grant only the scopes the integration uses.
- One key per integration. Separate keys mean a leak or a revocation affects one system, and audit logs attribute activity cleanly.
- Start read-only. Ship against the default read scopes, then add write scopes as flows come online.
- Reserve
admin:platform. Administrative tools only.