Authentication¶
The Telbox developer API uses two credentials. API keys (tb_live_…), minted
in the developer console, authenticate your own workspace machine-to-machine,
gated by deny-by-default scopes (§1–2). OAuth 2.1 tokens authenticate a
third-party app or agent acting on behalf of a Telbox user, and are what every
MCP endpoint requires (§3). Both are tb_live_… bearers; they differ in how
they're minted and what they're bound to.
Gated until launch
The external API-key auth path is behind the apikey_auth_enabled flag and
is dark on the public production host until launch. While it's off, point
your base URL at your developer preview. Everything below describes the
enabled behavior.
1. Create a developer account¶
The console sign-up is email + password + a 6-digit email code. These four
endpoints are public (they mint your console session) and live under
/v1/developer/auth.
| Endpoint | Body | Returns |
|---|---|---|
POST /v1/developer/auth/register |
{ email, password, display_name } |
{ email, needs_verification: true, dev_code? } |
POST /v1/developer/auth/verify |
{ email, code } |
{ email, access_token } |
POST /v1/developer/auth/login |
{ email, password } |
{ email, access_token } |
POST /v1/developer/auth/resend |
{ email } |
{ email, needs_verification: true, dev_code? } |
passwordmust be at least 10 characters;emailanddisplay_nameare required at registration.registercreates the account and emails a 6-digit code.verifyconfirms the code, marks the email verified, and returns a console sessionaccess_token.loginrequires a verified email; if the account exists but was never verified, it re-sends a code and returnsneeds_verification: trueinstead of a token.
dev_code in non-prod
In non-production environments (no SMTP configured) the verification code is
echoed back as dev_code so the local preview works without a mail server.
Production sends a real email and never echoes the code.
curl -X POST https://api.telbox.ai/v1/developer/auth/register \
-H "Content-Type: application/json" \
-d '{ "email": "ada@example.com", "password": "correct-horse-battery", "display_name": "Ada" }'
curl -X POST https://api.telbox.ai/v1/developer/auth/verify \
-H "Content-Type: application/json" \
-d '{ "email": "ada@example.com", "code": "123456" }'
# → { "email": "ada@example.com", "access_token": "eyJhbGciOi…" }
The console access_token authenticates the first-party developer endpoints
(/v1/developer/*, /v1/agents/*) that you use to manage keys and agents from
the portal. For calling the API from your own server, mint an API key (next).
2. API keys¶
An API key is the credential your integration uses. The format is
tb_{env}_{token} — tb_live_… on the production host, tb_test_… in non-prod
— and only an argon2 hash plus a short display prefix (tb_{env}_{token[:8]})
is ever stored. The env is chosen by the server from the environment; you don't
pick it.
Shown exactly once
The raw key is returned only in the create/rotate response. Store it in a secret manager immediately — you cannot retrieve it again. Lost a key? Rotate or revoke it and mint a new one.
Manage keys — /v1/developer/api-keys¶
These are first-party endpoints, so call them with your console access_token
(or from the portal). They manage your own workspace's keys.
| Endpoint | Purpose |
|---|---|
GET /v1/developer/scopes |
The grantable scope catalog |
GET /v1/developer/api-keys |
List your keys (newest first) |
POST /v1/developer/api-keys |
Create a scoped key → raw key (once) |
POST /v1/developer/api-keys/{key_id}/rotate |
Mint a replacement + revoke the old |
DELETE /v1/developer/api-keys/{key_id} |
Revoke a key |
POST /v1/developer/api-keys takes { name, scopes?, expires_at? }. If you omit
scopes, the key defaults to ["threads:read", "messages:write",
"voice_notes:write"]. expires_at is an optional ISO-8601 expiry. The response
is { key, raw_key, env }, where key is the key's metadata view and raw_key
is the full secret.
One-shot key
POST /v1/agents/api-keys mints a single default-scoped key
(threads:read, messages:write, voice_notes:write) for your workspace in
one call — the portal's "Get an API key" button. Use
POST /v1/developer/api-keys when you want to choose the name, scopes, or an
expiry.
Create a key¶
import { TelboxClient } from "@telbox/sdk";
// Authenticate the SDK with your console session to mint a key, once.
const admin = new TelboxClient({ apiKey: "<console_access_token>" });
const created = await admin.createApiKey(); // default scopes, raw key once
console.log(created.rawKey); // tb_live_… — stash this now
Use a key¶
Send the key as a Bearer token on every request. The SDKs take it once at construction.
Rotate and revoke¶
Rotating mints a replacement key (same name, scopes, and expiry) and revokes the
old one in a single call — update your secret store with the new raw_key, then
the old key stops working. Revoking is immediate and irreversible.
# Rotate: returns a new raw_key; the old key_id is revoked.
curl -X POST https://api.telbox.ai/v1/developer/api-keys/{key_id}/rotate \
-H "Authorization: Bearer <console_access_token>"
# Revoke: 204 No Content, no body.
curl -X DELETE https://api.telbox.ai/v1/developer/api-keys/{key_id} \
-H "Authorization: Bearer <console_access_token>"
Every create, rotate, and revoke is rate-limited and recorded in the platform audit log.
Scopes¶
Keys are deny-by-default: a key can only do what it was explicitly granted.
Fetch the live catalog from GET /v1/developer/scopes. The vocabulary:
| Scope | Grants |
|---|---|
threads:read |
List threads and enumerate message metadata (ids, kinds, sender, timestamps, delivery state). Encrypted content / transcripts / audio URLs are stripped unless the key also holds messages:read.raw. |
messages:read.raw |
Raw decrypted message + voice content (high-privilege). |
voice_notes:read |
Transcripts, summaries, and extracted actions + their metadata. |
messages:write |
Send text messages (non-voice-note kinds). |
voice_notes:write |
Create / send a Telbox voice note. |
tasks:write |
Create a task. |
contacts:read |
Read contacts. |
agents:read |
List templates/tools/agents and read agent run traces. |
agents:write |
Create, compile, publish, test-run, or delete agents. |
webhooks:manage |
Manage webhook endpoints. |
scim |
SCIM provisioning. |
Building agents with an API key
The agent-build endpoints (/v1/agents*, /v1/agent-templates,
/v1/agent-tools) accept an API key that holds agents:read (reads) or
agents:write (mutations) — select those scopes when you create the key.
POST /v1/agents/api-keys (minting a key) stays first-party-only, so mint
keys from the developer console, not with another key.
messages:read.raw exposes plaintext
threads:read is the metadata-only sibling — "show me what happened" without
"show me what was said." Grant messages:read.raw only when the integration
genuinely needs decrypted content; it's the high-privilege scope and is
audited.
3. OAuth for MCP & third-party apps¶
API keys authenticate your own workspace. When a third-party app or agent
acts on behalf of a Telbox user — and whenever you call an MCP endpoint —
it authenticates with an OAuth 2.1 token instead. The token is still a
tb_live_… bearer, but it is minted through an OAuth flow and bound to a client
plus a user-approved scope ceiling. A console-issued API key is rejected on
/mcp with oauth_token_required.
Telbox is a standards OAuth 2.1 authorization server: RFC 7591 dynamic client
registration, PKCE-S256 mandatory, refresh-token rotation, the RFC 8628
device flow, and RFC 7009 revocation. Every endpoint lives under /v1/oauth
and — like the rest of the platform — returns 404 developer_platform_disabled
until apikey_auth_enabled is on. OAuth errors use a { "detail": "<code>:
<reason>" } body.
Pick your flow:
- Authorization code + PKCE (§3.2) — apps that can receive a browser redirect (web, mobile, desktop with a loopback URL).
- Device flow (§3.3) — CLIs and headless integrations with no redirect; this is how OpenClaw authenticates.
3.1 Register a client (once)¶
POST /v1/oauth/register (RFC 7591) is public and returns a client_id. Public
clients (token_endpoint_auth_method: "none") use PKCE and get no secret;
confidential clients ("client_secret_basic") also receive a client_secret.
curl -X POST https://api.telbox.ai/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Assistant",
"redirect_uris": ["https://acme.example/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"scope": "threads:read messages:write"
}'
# → { "client_id": "…", "client_secret": null,
# "scope": "threads:read messages:write", … }
- Register the
grant_typesyou'll actually use (authorization_code,refresh_token, and/ordevice_code) — a device-only client still registers at least oneredirect_uri(a loopback is fine). - A loopback
redirect_uri(http://127.0.0.1:PORT/cb, RFC 8252) is allowed in dev; production requireshttps.client_namemay not contain reserved words (telbox, official, claude, …).
Approval
software_id: "openclaw" is auto-approved. Every other client is created
pending manual review, and /authorize (or the device flow) refuses an
unapproved client — so register early.
3.2 Authorization code + PKCE¶
For an app that can catch a browser redirect.
- Build a PKCE pair:
code_verifier= 43–128 random unreserved chars;code_challenge= base64url( SHA-256(code_verifier) ), no padding. - Send the user's browser to
/authorize(code_challenge_method=S256is mandatory):
https://api.telbox.ai/v1/oauth/authorize
?client_id=<client_id>
&redirect_uri=https://acme.example/callback
&response_type=code
&scope=threads:read%20messages:write
&state=<opaque-random>
&code_challenge=<challenge>
&code_challenge_method=S256
- The user reviews the requested scopes and approves (or denies) inside
Telbox. Telbox redirects the browser back to
https://acme.example/callback?code=<code>&state=<state>— a denial comes back as?error=access_denied&state=…. Verifystatematches what you sent. - Exchange the code (form-encoded) with the same
code_verifier:
curl -X POST https://api.telbox.ai/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d client_id=<client_id> \
-d code=<code> \
-d redirect_uri=https://acme.example/callback \
-d code_verifier=<code_verifier>
# → { "access_token": "tb_live_…", "token_type": "Bearer", "expires_in": 3600,
# "refresh_token": "…", "scope": "threads:read messages:write" }
A confidential client instead authenticates with
-u <client_id>:<client_secret> (HTTP Basic).
3.3 Device flow (CLIs & headless)¶
For a CLI with no browser redirect — the OpenClaw path.
- Request a device + user code (form-encoded, public):
curl -X POST https://api.telbox.ai/v1/oauth/device_authorization \
-H "Content-Type: application/x-www-form-urlencoded" \
-d client_id=<client_id> \
-d scope="threads:read messages:write"
# → { "device_code": "…", "user_code": "ABCD-EFGH",
# "verification_uri": "https://telbox.ai/oauth/device",
# "verification_uri_complete": "https://telbox.ai/oauth/device?user_code=ABCD-EFGH",
# "expires_in": 900, "interval": 5 }
- Tell the user to open
verification_uriand enteruser_code(or openverification_uri_completedirectly). They approve in the Telbox app. - Poll the token endpoint every
intervalseconds with the device grant:
curl -X POST https://api.telbox.ai/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d device_code=<device_code> \
-d client_id=<client_id>
While the user hasn't finished, this returns 400 with
{"detail":"authorization_pending"} (or slow_down — then back off by the
Retry-After seconds). Once approved it returns the same token body as §3.2.
OpenClaw self-configures from the manifest
OpenClaw fetches GET /v1/integrations/openclaw/manifest — a public
descriptor listing the exact call order (register → device_authorization
→ device/decide → token → /mcp), the scope union, and the six tools —
then runs the device flow above with software_id: "openclaw" (auto-approved).
3.4 Use, refresh, and revoke¶
Send the OAuth access_token as a Bearer to the MCP endpoints:
curl -X POST https://api.telbox.ai/mcp \
-H "Authorization: Bearer tb_live_…" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The same token authenticates a published agent's own endpoint at
POST /v1/agents/{agent_id}/mcp. See MCP for the tool
surface, the per-agent exposed-tool subset, and the outbound broker.
-
Refresh — access tokens live
3600 s. Rotate with the refresh token (single-use; reusing a rotated token revokes the whole token family): -
Revoke —
POST /v1/oauth/revoke(formtoken=<token>) always returns200with an empty body; it deliberately never reveals whether the token was valid.
OAuth scopes are the same vocabulary
An OAuth token carries the same scopes as an API key (§2 catalog) — the user
only ever grants scopes the client registered for. An auto-approved client's
ceiling excludes the high-privilege messages:read.raw and scim.
Full request/response schemas for every /v1/oauth/* endpoint (and /mcp) are in
the API Reference.
Errors¶
A missing or malformed credential returns 401 with a machine-readable
error code. Unknown scopes at key creation return 400
unknown_scopes: …; calling a first-party endpoint without a valid console
session returns 401/403. See Errors and
Rate Limits & Quotas.
Next steps¶
- Getting Started — base URL, conventions, your first request.
- MCP — the OAuth-gated per-agent MCP endpoint.
- Webhooks — subscribe to events with
webhooks:manage. - Rate Limits & Quotas — per-developer key-mutation and write budgets.