API reference

Authentication

Workspace API keys, how to rotate them, and what a rejected request looks like.

Authentication

Every request to the Product API carries a workspace API key as a bearer token.

Authorization: Bearer cap_a1b2c3d4e5f6.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

A key is two parts joined by a dot: a public cap_-prefixed identifier and a 48-character secret. Only the prefix is stored in the clear — the secret is hashed, so we can look a key up quickly without being able to read it back.

Keys are workspace-scoped

A key belongs to exactly one workspace and can never read or write another's data. There is no account-level or cross-workspace key. If you operate several workspaces, you hold several keys.

Scopes

Each key carries a set of scopes, checked on every request:

Scope Grants
contacts:read GET /workspace, GET /contacts
contacts:write POST /contacts
messages:write POST /messages, POST /media
messages:read GET /messages, GET /messages/{id}
media:read GET /media/{id}, GET /media/{id}/download
groups:read GET /groups, POST /groups/sync
groups:send POST /groups/{id}/messages
templates:read GET /templates, GET /templates/{id}, GET /templates/{id}/preview
templates:write POST /templates, PUT /templates/{id}, DELETE /templates/{id}
campaigns:read GET /campaigns, GET /campaigns/{id}, GET /campaigns/{id}/recipients, POST /campaigns/audience/preview
campaigns:write POST /campaigns, PUT /campaigns/{id}, DELETE /campaigns/{id}
campaigns:send POST /campaigns/{id}/schedule, /pause, /resume, /cancel

Grant only what an integration needs. A reporting job that never sends should hold contacts:read alone — then a leaked key cannot message your customers. Sending and reading inbound files are separate on purpose: a key that only delivers notifications never gains the ability to pull your customers' photos and documents back out.

Messaging a group is separate from messages:write for the same reason. One call to a group reaches every member at once, and those members never opted in to anything with you — a key issued to send order updates should not silently be able to post to a group of five hundred. See Groups.

campaigns:send is split from campaigns:write on the same principle, one step further: scheduling or resuming a campaign fires messages at an entire audience. A CMS integration that keeps drafts in sync holds campaigns:write and can never launch one; the thing that launches holds campaigns:send and is the credential you guard hardest.

Creating a key without choosing scopes grants all twelve.

Scopes are stamped when the key is issued. A key created before a scope existed does not gain it later — to call newer endpoints (templates, campaigns), issue a fresh key.

Creating and revoking

Dashboard → Developers → API keys.

The plaintext key is displayed once, at creation. We store only a hash, so "show me my key again" is not a feature we can build — recovery is always create a new one, revoke the old one.

Revoking takes effect immediately: the next request gets 401. A revoked key can then be removed from the list entirely. Both actions are recorded in the workspace audit log.

Rotate by creating the new key first, deploying it, then revoking the old one. That order means no window where neither key works.

Check a key works

Read the workspace the key belongs to. It is the cheapest way to confirm a new key is live before you build against it, and it needs only contacts:read.

GET /api/v1/workspace
{
  "data": {
    "id": "3f1c0e2a-6d3a-4c1b-9f7e-2a1d8c0b5e33",
    "name": "Karachi Electronics",
    "slug": "karachi-electronics",
    "status": "active"
  }
}

A 200 tells you two things at once: the key is valid, and its workspace is active. You will only ever see "status": "active" here — a workspace that is not active is refused at authentication, so it returns the 403 below on this call too, rather than a 200 carrying some other status.

Rejected requests

Condition Status Body
Missing, malformed, unknown or revoked key 401 {"message":"A valid API bearer token is required."}
Workspace inactive 403 {"message":"The API key workspace is unavailable."}
Valid key, scope not granted 403 {"message":"The API key does not have the required scope."}

An unknown key and a revoked key return the identical 401. Distinguishing them would tell an attacker which guesses were once valid.

Rate limits

Limiter Default Applies to
Per key 60 requests/minute Every authenticated request
Per IP, failed auth 120/minute Requests with an invalid bearer

Both windows are 60 seconds.

Exceeding the per-key limit returns:

{ "message": "The API key rate limit has been reached." }

Exceeding the failed-auth limit returns a different message, because it is a different condition — too many bad credentials from one address, not too much traffic from one key:

{ "message": "Too many invalid API bearer attempts." }

There is no Retry-After header today. Back off on your own schedule — exponential with jitter, starting around a second — rather than waiting for a header to tell you when.

Note that a request rejected for a missing scope still consumes per-key quota: the key was valid, the permission was not.

Keeping the key secret

  • Server-side only. A key in browser JavaScript or a mobile binary is a published key, whatever the app does to hide it.
  • Not in source control. Use environment variables or a secret manager.
  • One key per integration. Separate keys for your CRM, your order system and your staging environment mean you can revoke one without taking down the others — and the audit log tells you which system did what.

We never log the bearer token or raw request bodies.

Webhook secrets are different

The signing secret on a webhook endpoint is not an API key and is not interchangeable with one. It never authenticates a request to us — it lets you verify that a request came from us. See Signature verification.

Was this guide useful? Report an issue