Get started
Authentication and keys
API keys, console session, scopes, environments, and security best practices.
Updated November 12, 2026View as Markdown
Two kinds of credentials#
| Console session | API key | |
|---|---|---|
| Format | ses_… | pk_test_… (sandbox) or pk_live_… (production) |
| Used for | Managing your account: apps, keys, billing, logs | Calling the API from your server: connections, messages, templates, webhooks, usage |
| Obtained via | POST /auth/signup or POST /auth/login | POST /apps/{appId}/keys |
| Lifetime | 30 days, revocable (POST /auth/logout) | Until revoked |
| Header | Authorization: Bearer ses_… | Authorization: Bearer pk_… |
The routes in the "Authentication" and "Apps" sections, GET /billing/overview, and GET /logs/requests use the session. All the others use an API key. Each page of the reference states its authentication mode.
The console uses the same routes#
The API-key routes (connections, messages, templates, webhooks, usage) also accept the console session, with an X-Environment-Id header that specifies the target environment (an environment's identifier is returned by GET /apps). The signed-in member then has all the scopes of their account. This is what the Bref Point Inbox console does. These calls do not appear in the request log, which is reserved for API keys.
For use from your server, always use a key: its rights are limited by scope and its environment is fixed.
Create a key#
curl -X POST "$INBOX_API/apps/APP_ID/keys" \
-H "Authorization: Bearer $INBOX_SESSION" \
-H "Content-Type: application/json" \
-d '{
"name": "Local dev",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
]
}'const res = await fetch(`${process.env.INBOX_API}/apps/APP_ID/keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_SESSION}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Local dev",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
]
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/apps/APP_ID/keys",
headers={
"Authorization": f"Bearer {os.environ['INBOX_SESSION']}",
},
json={
"name": "Local dev",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read",
],
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/apps/APP_ID/keys');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_SESSION'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Local dev',
'environment' => 'sandbox',
'scopes' => [
'messages:send',
'messages:read',
'connections:manage',
'connections:read',
'templates:manage',
'webhooks:manage',
'usage:read',
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"name": "Local dev",
"prefix": "pk_test_bwcA3X-A",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
],
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-11-12T08:33:45.382Z",
"secret": "pk_test_26f602c48c47ef280792fd3a506473688df70b80118"
}Scopes#
A key can only do what you grant it. A call that requires a scope the key doesn't have is rejected with 403 forbidden, and the message names the missing scope.
| Scope | Allows |
|---|---|
messages:send | Sending messages (POST /messages) and using the sandbox simulator |
messages:read | Reading a message or the history (GET /messages) |
connections:manage | Creating connected businesses and connecting WhatsApp accounts |
connections:read | Listing connected businesses, their accounts, and their numbers |
templates:manage | Creating, listing, syncing, and deleting templates |
webhooks:manage | Managing webhook endpoints and viewing/replaying deliveries |
usage:read | Reading usage (GET /usage) |
Environments#
Each app has a sandbox environment and a production environment. A key belongs to one environment only and sees only its data: connected businesses, numbers, messages, webhook endpoints.
- A
pk_test_…key can never touch production, and vice versa. - The sandbox simulator rejects production keys (
403 sandbox_only). - The sandbox is neither rate limited nor billed.
Use a key#
curl "$INBOX_API/messages" -H "Authorization: Bearer $INBOX_KEY"A missing, invalid, or revoked key returns 401 unauthorized.
{
"error": {
"code": "unauthorized",
"message": "API key required (Authorization: Bearer pk_…)",
"requestId": "req_1c1b9300-f2d4-4387-88b8-7d66222d7497"
}
}List and revoke#
curl "$INBOX_API/apps/APP_ID/keys" \
-H "Authorization: Bearer $INBOX_SESSION"const res = await fetch(`${process.env.INBOX_API}/apps/APP_ID/keys`, {
headers: {
Authorization: `Bearer ${process.env.INBOX_SESSION}`,
},
});
const data = await res.json();import os
import requests
res = requests.get(
f"{os.environ['INBOX_API']}/apps/APP_ID/keys",
headers={
"Authorization": f"Bearer {os.environ['INBOX_SESSION']}",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/apps/APP_ID/keys');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_SESSION'),
],
]);
$data = json_decode(curl_exec($ch), true);[
{
"id": "5ef37bc9-8aac-43ef-81b1-6bb3bdb66e78",
"name": "Local dev",
"prefix": "pk_test_bwcA3X-A",
"environment": "sandbox",
"scopes": [
"messages:send",
"messages:read",
"connections:manage",
"connections:read",
"templates:manage",
"webhooks:manage",
"usage:read"
],
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-11-12T08:33:45.382Z"
}
]lastUsedAt shows the last use: a key that hasn't been used for weeks is a good candidate for revocation.
curl -X DELETE "$INBOX_API/apps/APP_ID/keys/KEY_ID" \
-H "Authorization: Bearer $INBOX_SESSION"const res = await fetch(`${process.env.INBOX_API}/apps/APP_ID/keys/KEY_ID`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.INBOX_SESSION}`,
},
});
const data = res.status;import os
import requests
res = requests.delete(
f"{os.environ['INBOX_API']}/apps/APP_ID/keys/KEY_ID",
headers={
"Authorization": f"Bearer {os.environ['INBOX_SESSION']}",
},
)
print(res.status_code)<?php
$ch = curl_init(getenv('INBOX_API') . '/apps/APP_ID/keys/KEY_ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_SESSION'),
],
]);
curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE);(no content)Revocation is immediate and permanent.
Best practices#
- Call the API from your server, never from a browser or a mobile app. A key placed in client code is public. The API also doesn't allow browser calls (no CORS).
- Store keys in environment variables or a secrets manager, never in source code or a repository.
- Use one key per service and per environment: you can revoke one without interrupting the others.
- Rotate your keys: create the new one, deploy it, check that the old key's
lastUsedAtno longer changes, then revoke it. - Monitor the request log: every call appears there with its status and its
requestId.