Authentication and keys
Bearer API keys, scopes, live and test environments, App pinning, IP allowlists, expiry, rotation, and what happens when a key's owner leaves.
Every request is authenticated with an API key, sent as a bearer token:
GET /backend/v2/me HTTP/1.1
Host: app.gauge.chat
Authorization: Bearer pulse_k_live_9c2Rk1...A request without a key, or with an unknown, revoked or expired one, gets 401 unauthorized.
Who can create keys
- The workspace must be on a paid plan. The API is available on Growth and Scale. On
Free, every request answers
402 plan_required(Plans). - The member must be allowed to manage API keys. Keys are created in Developers → API keys by a member whose role includes the API keys permission.
Keys belong to a person
A key is not a free-floating credential. It acts as the member who created it:
- It can only do what that member can do in the console. A key never sees a conversation its member could not open, so a key made by an Agent who sees only their own conversations sees only those.
- Contacts are the exception the console already makes: every member with contact access sees the whole workspace's contacts, and so does their key.
- If the member is deactivated or removed, every key they made stops working at once with
401 key_owner_inactive. Create a new key under an active member and swap it in.
Plan for people leaving
Create integration keys from a member who will stay, such as an owner or a dedicated integration seat, and keep a note of who owns which key. The API keys list shows each key's creator.
Scopes
A key carries a list of scopes; each endpoint needs one. The scope a route needs is shown on every
reference page (x-scopes). A key's power is its scopes and its member's role,
together: a scope the member's role cannot use grants nothing.
| Scope | Lets the key | Needs this in the member's role |
|---|---|---|
conversations:read | List and open conversations the member can see | Conversations · view |
conversations:write | Change status, assignee and tags; mark read | Conversations · view, create, edit |
messages:read | Read message history and delivery status, and download a message's files | Conversations · view |
messages:write | Send messages and templates as the member | Conversations · view, create |
contacts:read | List, search and open contacts | Contacts · view |
contacts:write | Create, edit, upsert, tag and merge contacts. Erasing one (DELETE) also needs the member to hold the erasure right (Owner or Admin) | Contacts · view, create, edit |
connections:read | See connected channels and what each can send | Conversations · view |
templates:read | See approved WhatsApp templates | Conversations · view |
media:read | Get short-lived links to files you uploaded with POST /v2/media | Conversations · view |
media:write | Upload files to attach to messages | Conversations · view, create |
webhooks:read | List webhook endpoints and their delivery state | Settings · view |
webhooks:write | Create, change, test and rotate webhook endpoints; redeliver events | Settings · view, edit |
events:read | List and read the last 30 days of events | Settings · view |
Write includes read. messages:write also grants messages:read.
Wildcards. *:read grants every area's :read scope, and *:write grants every area scope.
They are capped at use to exactly what the member holds, so a wildcard can never reach further than
the member's role.
Spending scopes. sms-send and voice-dial are reserved for SMS and calls over the API,
which open after a compliance review. They are granted only by name: no wildcard, and no key
created as "full access", ever includes them.
You cannot grant what you do not have. When you create a key, the console offers only the
scopes your own role can use. Asking for more over the console API answers
400 validation_failed with "Your role cannot grant: …".
When a key calls a route without the scope it needs, the answer is 403 insufficient_scope, and
error.details.required names the missing scope.
Version 1 scopes
Keys made for /v1 carry v1 scopes such as contacts:rw or support:read. They keep working on
/v1. A v1 <area>:rw scope also counts as <area>:write on /v2.
Environments
Every key is live or test, fixed when it is created and visible in its prefix:
| Live key | Test key | |
|---|---|---|
| Prefix | pulse_k_live_ | pulse_k_test_ |
| Reaches | Your real workspace data | Sandbox data only |
GET /v2/me key.environment | live | test |
Test keys
A test key works only on endpoints that support the sandbox. Elsewhere it answers
403 forbidden with reason: sandbox_key_not_supported; nothing it does can reach a real
customer. Test keys are accepted by /v2/me, /v2/connections, /v2/templates and the
Support API.
Test keys are refused on every messaging and webhook route: contacts, conversations,
messages, media, /v2/webhook-endpoints and /v2/events. There is no sandbox send: a send always
reaches a real customer, so it needs a live key. To try a send safely, send to your own number or
address on a live key.
Key prefixes
Keys are issued as pulse_k_live_… and pulse_k_test_… today. A gauge_live_ / gauge_test_
prefix is planned; when it ships, existing keys keep working unchanged.
Pinning a key to an App
If you use the Support API, you can pin a key to one App (a support project). A
pinned key sees only that App's data: on /v2/connections, only the App's chat line. On /v2 a
key may be pinned to one App; a key pinned to several answers 403 forbidden with
reason: key_pinned_to_projects. GET /v2/me returns the pin as key.pinnedAppId.
A pinned key can read /v2/me and /v2/connections, and manage the webhook endpoints and events of
its own App. It is refused on the messaging routes (contacts, conversations, messages, media,
templates) with 403 forbidden and reason: key_pinned_to_projects: a contact or a conversation
belongs to no single App. Use an unpinned key for messaging.
IP allowlist
Give a key a list of up to 20 IPv4 or IPv6 addresses or CIDR ranges (203.0.113.7,
198.51.100.0/24, 2001:db8::/32), and it works only from those addresses. A request from
anywhere else answers 403 forbidden with reason: ip_not_allowed. An empty list means any
address.
Expiry
A key can expire after 30, 60, 90 or 365 days, or never. After its expiry it answers
401 unauthorized. Expiring keys are a good default for anything that is not a long-lived server
integration.
Rotating a key
Rotate (roll) a key from its row in Developers → API keys. Rolling issues a new secret for the same key: same name, scopes, pin, allowlist and usage history. You choose how long the old secret keeps working alongside the new one:
| Overlap | Use it when |
|---|---|
| 0 hours | The key leaked. The old secret stops at once. |
| 1 hour | You can redeploy within the hour. |
| 24 hours | A normal scheduled rotation. |
| 72 hours | Several systems share the key and take time to update. |
During the overlap both secrets authenticate. Deploy the new one, then let the old one lapse.
To stop a key for good, revoke it. Revocation is immediate and cannot be undone.
Keeping keys safe
- Keep keys on your server. Never ship one in a browser bundle, a mobile app, or a public repository.
- One key per integration, so you can rotate or revoke one without breaking the others.
- Grant the fewest scopes that work, and add an IP allowlist for fixed servers.
- Every request a key makes is in Developers → Logs for 14 days, by key, status and request id.
Authentication errors
| Status | error.code | reason | Meaning |
|---|---|---|---|
| 401 | unauthorized | No key, or an unknown, revoked, expired or rolled-out key | |
| 401 | key_owner_inactive | key_owner_inactive | The key's member is no longer active |
| 402 | plan_required | The workspace is not on Growth or Scale | |
| 403 | workspace_suspended | The workspace is suspended; contact Gauge support | |
| 403 | forbidden | ip_not_allowed | The request came from outside the key's allowlist |
| 403 | forbidden | sandbox_key_not_supported | A test key on an endpoint without sandbox support |
| 403 | forbidden | key_pinned_to_projects | A pinned key on an endpoint that cannot honour the pin |
| 403 | insufficient_scope | The key, or its member's role, lacks the route's scope |
The full list is in Errors.