Skip to content

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:

Terminal window
curl https://app.pufferpost.com/v1/whoami \
-H "Authorization: Bearer key_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

For requests with a body, also set the content type:

Terminal window
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:

  1. Open your workspace in the console and go to Settings → API keys.
  2. Give the key a name (so you can recognise it later in listings and audit logs).
  3. Choose the environmentlive or test.
  4. Tick the scopes the key needs (see below). The default selection is messages:send and messages:read, which covers the common “send and look up your own messages” case.
  5. 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 environment
key_test_8f3aC2k9... ← test environment

The format is key_<environment>_<secret>:

  • key_ — fixed prefix shared by every key.
  • <environment>live or test.
  • <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

ResourceScopesWhat it gates
Messagesmessages:sendSend a single message and batch sends (POST /v1/messages, POST /v1/messages/batch)
messages:readRetrieve a message, list messages, list a message’s events
Templatestemplates:readGet, list, and preview templates
templates:manageCreate templates, add versions, clone
Senderssenders:readGet and list senders
senders:manageRegister a sender, request verification
Domainsdomains:readList domains
domains:manageCreate a domain, refresh / request verification
Suppressionssuppressions:readList suppression entries
suppressions:manageAdd and delete suppression entries
Webhookswebhooks:readList endpoints and deliveries
webhooks:manageCreate / delete endpoints, replay a delivery
Unsubscribe groupsunsubscribe_groups:readList unsubscribe groups
unsubscribe_groups:manageCreate and delete unsubscribe groups
Billing & usagebilling:readRead the subscription and list plans
billing:manageStart a billing checkout
usage:readRead usage metering
Workspaceworkspace:readRead workspace settings
workspace:manageUpdate workspace settings
Contactscontacts:readGet / list contacts and a contact’s messages
recipients:readExport a recipient’s data
recipients:eraseErase a recipient’s data
Logs & auditaudit:readRead the audit log
request_logs:readRead the developer API request logs
Workflowsworkflows:triggerTrigger 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.

Terminal window
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:

  1. Create a new key in the same environment with the same scopes.
  2. Deploy the new secret to your application.
  3. Confirm traffic is flowing on the new key (check last_used_at in the console, or call whoami with it).
  4. 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_at in 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.