Authentication
Every request to the PufferPost API is authenticated with a bearer API key. Keys are scoped, environment-specific (live or test), and tied to a single workspace — the unit of sending isolation. A key only ever grants access to its own workspace’s data.
There is no username/password or OAuth flow for the API: you create a key in the dashboard, store its secret, and send it in the Authorization header.
The Authorization header
Send your key as a bearer token on every request:
curl https://app.pufferpost.com/v1/whoami \ -H "Authorization: Bearer key_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"For requests with a body, also set the content type:
curl https://app.pufferpost.com/v1/messages \ -H "Authorization: Bearer key_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "...": "..." }'A missing, malformed, expired, or revoked key gets a 401 Unauthorized:
{ "error": "Unauthorized" }Creating a key
API keys are minted from the dashboard console, not over the API — there is no endpoint that creates keys, which keeps the secret-bearing path off the public surface.
To create one:
- Open your workspace in the console and go to Settings → API keys.
- Give the key a name (so you can recognise it later in listings and audit logs).
- Choose the environment —
liveortest. - Tick the scopes the key needs (see below). The default selection is
messages:sendandmessages:read, which covers the common “send and look up your own messages” case. - Create the key. The plaintext secret is shown exactly once — copy it immediately into your secret store.
Only the secret’s lookup hash is stored server-side (a peppered SHA-256); PufferPost never persists or can re-display the plaintext. If you lose it, create a new key and revoke the old one. Creating and revoking keys requires the owner or admin role on the workspace.
Listings show only non-secret metadata — the key’s id, name, prefix, last4, scopes, environment, created_at, and last_used_at — never the full secret.
Key format
A key looks like this:
key_live_8f3aC2k9... ← live environmentkey_test_8f3aC2k9... ← test environmentThe format is key_<environment>_<secret>:
key_— fixed prefix shared by every key.<environment>—liveortest.<secret>— a 48-character random string. This is the part that must stay secret.
The full leading segment (e.g. key_live_8f3aC2k9) is the prefix surfaced in listings so you can tell keys apart at a glance; the last 4 characters of the secret are also shown as last4. Neither reveals the full secret.
Separately from the secret, each key has a stable public id of the form key_… (a prefixed ULID) used to reference it in the console and audit logs. The id is not a credential — only the key_live_… / key_test_… secret authenticates.
Live vs test environments
Each key is bound to one environment, and the environment is part of the secret string:
key_live_…— production keys. Use these for real sending.key_test_…— test keys, for development and CI.
Environments are isolated: a test key and a live key are different credentials with their own scopes and their own data. Pick the environment when you create the key; it can’t be changed afterwards — create a new key for the other environment.
Scopes
A scope is a fine-grained permission string in the form resource:action. A key carries a list of scopes, and each endpoint requires one specific scope verbatim. Scopes are not hierarchical: holding templates:manage does not imply templates:read — if a key needs to both read and write templates, grant it both.
If an authenticated key calls an endpoint it lacks the scope for, the request is rejected with 403 Forbidden:
{ "error": { "code": "insufficient_scope", "message": "The API key lacks the scope required for this endpoint." }}Available scopes
| Resource | Scopes | What it gates |
|---|---|---|
| Messages | messages:send | Send a single message and batch sends (POST /v1/messages, POST /v1/messages/batch) |
messages:read | Retrieve a message, list messages, list a message’s events | |
| Templates | templates:read | Get, list, and preview templates |
templates:manage | Create templates, add versions, clone | |
| Senders | senders:read | Get and list senders |
senders:manage | Register a sender, request verification | |
| Domains | domains:read | List domains |
domains:manage | Create a domain, refresh / request verification | |
| Suppressions | suppressions:read | List suppression entries |
suppressions:manage | Add and delete suppression entries | |
| Webhooks | webhooks:read | List endpoints and deliveries |
webhooks:manage | Create / delete endpoints, replay a delivery | |
| Unsubscribe groups | unsubscribe_groups:read | List unsubscribe groups |
unsubscribe_groups:manage | Create and delete unsubscribe groups | |
| Billing & usage | billing:read | Read the subscription and list plans |
billing:manage | Start a billing checkout | |
usage:read | Read usage metering | |
| Workspace | workspace:read | Read workspace settings |
workspace:manage | Update workspace settings | |
| Contacts | contacts:read | Get / list contacts and a contact’s messages |
recipients:read | Export a recipient’s data | |
recipients:erase | Erase a recipient’s data | |
| Logs & audit | audit:read | Read the audit log |
request_logs:read | Read the developer API request logs | |
| Workflows | workflows:trigger | Trigger a transactional workflow |
Grant the minimum set a given integration needs. A worker that only sends mail should hold messages:send (plus messages:read if it polls status) and nothing else.
The GET /v1/whoami endpoint requires no scope — any valid, non-revoked key can call it.
Verifying a key: GET /v1/whoami
Use whoami to confirm a key is valid and authenticated. It’s the simplest way to check credentials from a script or a health check.
curl https://app.pufferpost.com/v1/whoami \ -H "Authorization: Bearer key_live_8f3aC2k9..."On success it returns the calling key’s public id:
{ "api_key": "key_01J9Z3QH8M4P6R8T0V2X4Z6B8D" }A 200 with this body means the key authenticated. A 401 means it didn’t (wrong, revoked, or missing key). This call also updates the key’s last_used_at, which you can see in the dashboard listing.
Rotation and revocation
Keys are revoked from the dashboard console (Settings → API keys), the same place you create them. Revocation is instant: once revoked, the key’s very next request gets a 401, with no grace period.
To rotate a key with zero downtime:
- Create a new key in the same environment with the same scopes.
- Deploy the new secret to your application.
- Confirm traffic is flowing on the new key (check
last_used_atin the console, or callwhoamiwith it). - Revoke the old key.
Because keys are independent credentials, you can run several at once — one per service or per environment — and rotate them on independent schedules. Revoking one never affects the others.
Security best practices
- Server-side only. API keys are full-power credentials for a workspace. Never embed a key in a browser, mobile app, or any client you don’t control, and never commit one to version control. All API calls must originate from your backend.
- Use test keys in development and CI. Keep
key_live_…secrets out of test environments entirely. - Scope to the task. Issue a separate, narrowly-scoped key per integration rather than one all-powerful key. If a key leaks, the blast radius is just its scopes.
- Store secrets in a vault / secret manager, injected as environment variables at runtime — not in config files checked into a repo.
- Rotate periodically and immediately on any suspected exposure; revoke leaked keys at once.
- Watch
last_used_atin the console to spot keys that are unused (revoke them) or used unexpectedly.
The API is served over TLS only. See the API reference for full endpoint details and the errors guide for the complete error envelope and request_id handling.