Authentication — OAuth 2.1
Let your application call the Telebroad API on a user's behalf. The user approves the exact permissions you request; you receive a token — and never more access than that user already has. Your app never sees their password.
It's a standard OAuth 2.1 authorization-code + PKCE flow for public clients
(no client secret). Base URL: https://api.account.telebroad.com.
Scopes
Request only what you need — space-delimited in the authorize request.
| Scope | Grants |
|---|---|
webhooks:read | List and view webhooks |
webhooks:write | Create, update, delete, enable/disable webhooks |
reports:read | Read call history and analytics |
users:read | List the account's users — names, emails, extensions. Personal data. |
sms:read | List which of the account's numbers can text. Costs nothing and sends nothing. |
sms:write | Resolve a conversation, or attach an internal note to one. Cannot send anything to anyone. |
sms:send | Send SMS from the account's numbers — this spends money. Every message is billed to the account. Request it only if your app sends texts. |
recordings:read | Fetch links to call-recording audio. The most sensitive scope: it is the contents of conversations, not metadata. |
The three sms:* scopes are separate on purpose. Building a number picker
(sms:read) should not require the ability to spend money, a help-desk
integration that only closes threads (sms:write) should not be able to text
customers, and a send-only integration should not be able to enumerate the
account's numbers. Ask for each only if you do that thing.
The authoritative, live list is in the discovery document's scopes_supported.
A scope is a ceiling, not a key
Holding a scope does not mean you get everything under it. For an OAuth token, the portal permissions of the person who authorized your app still apply on top — a scope can only narrow what they could already do themselves, never widen it.
| Endpoint | Scope | And, for an OAuth token, the authorizing user's portal permission |
|---|---|---|
GET /users | users:read | Users — you see the users they can see, no more |
GET /sms/lines | sms:read | Phone numbers — only the numbers assigned to them |
POST /sms/conversations/… | sms:write | Phone numbers — the line must be one of theirs |
POST /sms/messages | sms:send | Phone numbers — from must be one of theirs |
GET /calls/{id}/recordings | recordings:read | Call reports → allow recordings; without it, 403 even with the scope |
So the same request can legitimately return different data for two tokens on the same account. If your app sees less than you expect, check the authorizing user's role in the portal before you assume a bug.
API keys are different. A key authenticates the account, not a person, so there is no user role to apply — a key gets the full account within its scopes. Use one for backend integrations that need account-wide reads; use OAuth when your app acts on behalf of a specific user.
One thing no credential can unlock: if the account has sensitive-content re-verification turned on, call recordings are unavailable over the API entirely. See Call recordings.
The flow
- Discover the endpoints (once).
- Register your app to get a
client_id(once). - Send the user to authorize with PKCE; they log in and approve.
- Receive the code at your redirect URI and exchange it for tokens.
- Call the API with the access token; refresh when it expires.
1. Discover
curl https://api.account.telebroad.com/.well-known/oauth-authorization-server
Returns the exact authorization_endpoint, token_endpoint,
registration_endpoint, and scopes_supported. Read them from here rather than
hardcoding.
2. Register (once)
The OAuth URLs shown below are what discovery returns today. They are the one part
of the platform not under /api/public/v1, so take them from the discovery
document at runtime rather than copying them — that way a future move costs you
nothing.
curl -X POST https://api.account.telebroad.com/api/v1/oauth/register \
-H 'Content-Type: application/json' \
-d '{"client_name":"Acme Webhooks","redirect_uris":["https://app.acme.example/oauth/callback"]}'
{ "client_id": "tbmcp_…", "redirect_uris": ["https://app.acme.example/oauth/callback"] }
Your redirect_uris must be HTTPS and their host must be allow-listed by
Telebroad — send us the callback host to add before going live.
3. Authorize (PKCE)
Per request, generate a PKCE pair:
code_verifier = base64url(32 random bytes) # keep secret, per-request
code_challenge = base64url( SHA-256(code_verifier) ) # sent in the URL
Send the user's browser to the discovered authorization_endpoint:
{authorization_endpoint}
?response_type=code
&client_id=tbmcp_…
&redirect_uri=https://app.acme.example/oauth/callback
&scope=webhooks:read%20webhooks:write
&state=RANDOM_PER_SESSION
&code_challenge=CHALLENGE
&code_challenge_method=S256
After they approve, the browser returns to
redirect_uri?code=…&state=… (or ?error=access_denied on deny). Verify the
returned state matches the one you sent.
4. Exchange the code
The token endpoint is application/x-www-form-urlencoded:
curl -X POST https://api.account.telebroad.com/api/v1/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
-d code_verifier=CODE_VERIFIER \
-d client_id=tbmcp_… \
-d redirect_uri=https://app.acme.example/oauth/callback
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "webhooks:read webhooks:write"
}
The access token lasts ~1 hour. Check scope — it's what was actually granted.
5. Refresh
curl -X POST https://api.account.telebroad.com/api/v1/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=refresh_token \
-d refresh_token=REFRESH_TOKEN \
-d client_id=tbmcp_…
Returns a new access token and a new refresh token — refresh tokens rotate, so always store the latest one.
Errors
| Status | Meaning → action |
|---|---|
401 | Missing/expired token → refresh or re-authorize |
403 insufficient scope: … | Token valid but lacks the scope → re-authorize with it |
400 on /token | Bad/expired code, PKCE mismatch, or wrong redirect_uri → restart |
Security notes
- PKCE (S256) is required — no client secret is issued.
- Redirect URIs must be HTTPS, exactly match a registered URI, and their host must be allow-listed (coordinate new hosts with Telebroad).
- Least privilege — a scope only narrows what the token can do; it can never exceed the approving user's own access. Request the minimum you need.
- Rotate refresh tokens — persist the newest; reusing an old one fails.