Appearance
Permissions
Each API key carries permissions written resource:action. A request whose permission the key lacks is rejected with 403 Forbidden: insufficient API key permissions before the handler runs, so nothing is read or written.
A key with no permissions can authenticate but reach nothing.
The matrix
Generated from the platform's permission catalog and operation inventory, so it always matches what the API enforces.
campaignsCampaigns
| Action | Unlocks |
|---|---|
campaigns:read |
|
campaigns:create |
|
listsLists
| Action | Unlocks |
|---|---|
lists:read |
|
lists:create |
|
recipientsRecipients
| Action | Unlocks |
|---|---|
recipients:read |
|
recipients:create |
|
recipients:import |
|
qr-codesQR codes
| Action | Unlocks |
|---|---|
qr-codes:read |
|
qr-codes:create |
|
qr-codes:update |
|
qr-codes:delete |
|
templatesTemplates
| Action | Unlocks |
|---|---|
templates:read |
|
Choosing permissions
Grant the minimum. campaigns:create spends real money — read-only keys should never hold it.
| Integration | Permissions |
|---|---|
| Send campaigns from templates, with recipients in the request | templates:read, campaigns:create, campaigns:read |
| Same, but reusing saved lists | add lists:read |
| Maintain an audience, send separately | lists:read, lists:create, recipients:read, recipients:create, recipients:import |
| Read-only reporting or a status dashboard | campaigns:read, lists:read, recipients:read |
| Manage response tracking | qr-codes:read, qr-codes:create, qr-codes:update |
Separate keys per job
A reporting service and a sending service should hold different keys: it limits the damage of a leak, and the per-key rate limit stops them throttling each other.
Notes on specific permissions
templates has only read. Templates are authored in the web app because printed output must be checked by a person. There is no templates:create, and mutation endpoints refuse API keys with 403 Forbidden: API keys cannot access this resource — no permission unlocks them. See Templates.
recipients:import is separate from recipients:create. create adds one recipient at a time; import accepts a whole file. A key that should only append individuals does not need import.
campaigns:create is the expensive one. It renders, prints and mails. Grant it only to the service that sends, and always pair it with an Idempotency-Key.
lists:read covers a list's field schema as well as the list collection — which you need before importing into an existing list.
QR codes have four independent actions. delete is the dangerous one — it makes already-printed codes resolve to a 404 forever. Most integrations want create, read and update but not delete. See QR codes.
Not governed by permissions
| Endpoint | Access |
|---|---|
GET /api | Public, no credentials |
GET /api/r/:slug | Public — it is the address printed on mail |
| API key management | Web app only; API keys always get 403 |
| Template create / update / delete | Web app only; API keys always get 403 |
Telling the two 403s apart
json
{ "error": { "code": "forbidden", "message": "Forbidden: insufficient API key permissions" } }Your key is fine, it just lacks a permission. Add it in the web app.
json
{ "error": { "code": "forbidden", "message": "Forbidden: API keys cannot access this resource" } }The endpoint is web-app-only. No permission will ever open it.
And a reminder from Conventions: 403 is only ever about permissions. A resource that exists but belongs to another account returns 404, never 403 — the API does not reveal that it exists.