Skip to content

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

ActionUnlocks
campaigns:read
  • GET /api/v1/campaigns
  • GET /api/v1/campaigns/status-counts
  • GET /api/v1/campaigns/:id
  • GET /api/v1/credits
  • GET /api/v1/credits/ledger
campaigns:create
  • POST /api/v1/campaigns

listsLists

ActionUnlocks
lists:read
  • GET /api/v1/lists
  • GET /api/v1/lists/:listId/schema
lists:create
  • POST /api/v1/lists

recipientsRecipients

ActionUnlocks
recipients:read
  • GET /api/v1/recipients
  • GET /api/v1/recipients/by-list/:listId
recipients:create
  • POST /api/v1/recipients
recipients:import
  • POST /api/v1/recipients/import/csv
  • POST /api/v1/recipients/import/excel

qr-codesQR codes

ActionUnlocks
qr-codes:read
  • GET /api/v1/qr-codes/:id
qr-codes:create
  • POST /api/v1/qr-codes
qr-codes:update
  • PATCH /api/v1/qr-codes/:id
qr-codes:delete
  • DELETE /api/v1/qr-codes/:id

templatesTemplates

ActionUnlocks
templates:read
  • GET /api/v1/templates
  • GET /api/v1/templates/:id

Choosing permissions ​

Grant the minimum. campaigns:create spends real money — read-only keys should never hold it.

IntegrationPermissions
Send campaigns from templates, with recipients in the requesttemplates:read, campaigns:create, campaigns:read
Same, but reusing saved listsadd lists:read
Maintain an audience, send separatelylists:read, lists:create, recipients:read, recipients:create, recipients:import
Read-only reporting or a status dashboardcampaigns:read, lists:read, recipients:read
Manage response trackingqr-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 ​

EndpointAccess
GET /apiPublic, no credentials
GET /api/r/:slugPublic — it is the address printed on mail
API key managementWeb app only; API keys always get 403
Template create / update / deleteWeb 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.