OAuth 2.1
For an application that acts on behalf of a user. They approve the exact permissions you ask for, you receive a token, and your app never sees their password — or more access than that user already has.
Standard authorization code + PKCE for public clients. No client secret is issued.
| Base URL | https://api.account.telebroad.com |
| Discovery | /.well-known/oauth-authorization-server |
| PKCE | Required, S256 |
| Access token | ~1 hour |
| Refresh token | Rotates on every use |
The flow
- Discover the endpoints — once.
- Register your app — once.
- Authorize — send the user to consent.
- Exchange the code for tokens.
- Refresh when the access token expires.
1. Discover
curl https://api.account.telebroad.com/.well-known/oauth-authorization-server
Returns the live authorization_endpoint, token_endpoint,
registration_endpoint and scopes_supported.
:::note Read the endpoints, don't copy them
The URLs below are what discovery returns today, and they are the one part of the
platform not under /api/public/v1. Take them from the discovery document at
runtime and a future move costs you nothing.
:::
2. Register
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 — with one standing exception: loopback callbacks always work, no
allow-listing and no HTTPS required. See below.
Register once, not per user. Store the client_id — there is no client
secret, and none is ever issued: this is a public client, and PKCE is what proves
the request came from you.
:::danger Your redirect URI must be allow-listed first
Registration fails if the redirect_uri host has not been allow-listed by
Telebroad. You get a 400:
{ "error": { "type": "invalid_request",
"message": "redirect_uri host … is not in the allowed redirect hosts" } }
Send us the exact callback URL before you register and we add the host.
Two rules that catch people out:
httpsonly for internet hosts. The scheme is checked before the host list, sohttp://app.acme.example/callbackis refused however the allow-list is configured. Loopback is the exception — see below.- The match is exact, and on the full URI. At authorize time your
redirect_urimust be character-for-character one of the values you registered — a trailing slash or anhttp/httpsswap is aredirect_uri not registered for this client. :::
Loopback callbacks always work
http://localhost:3000/callback
http://127.0.0.1:53682/callback
http://[::1]:8080/callback
localhost, 127.0.0.1 and [::1] are accepted on any port, over http, with
no allow-listing. Develop locally, and ship desktop and CLI apps, without
talking to us first.
This is RFC 8252 §7.3, loopback interface redirection. Any port is accepted because a native app binds whatever the OS gives it and cannot register the number in advance — so register the URI without pinning a port only if your library re-registers per run; the exact-match rule at authorize time still applies to whatever you did register.
:::note Why this is safe even though anyone can register a client
The authorization code is delivered to the user's own machine. An attacker who
registered a client pointing at 127.0.0.1 would be sending the code to the
victim's loopback interface, which they cannot reach — to read it they would
already need code running on that machine, and at that point the browser session is
theirs anyway.
That is not true of an internet host, which is exactly why those are allow-listed. :::
The exception matches the host, not a substring of it:
https://localhost.evil.example/cb is an ordinary internet host and still needs
allow-listing.
3. Authorize
Generate a PKCE pair per request:
code_verifier = base64url(32 random bytes) # secret, keep server-side
code_challenge = base64url( SHA-256(code_verifier) ) # goes 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
They return to redirect_uri?code=…&state=…, or ?error=access_denied if they
declined. Verify state matches the value you sent.
4. Exchange the code
Form-encoded, not JSON:
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"
}
Check scope — it is what was actually granted, which may be less than you
asked for.
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 — persist the newest one every time. Reusing an old one fails.
Call the API
curl https://api.account.telebroad.com/api/public/v1/webhooks \
-H "Authorization: Bearer ACCESS_TOKEN"
Errors
| Status | Meaning | Action |
|---|---|---|
401 | Missing or expired token | Refresh, then re-authorize if that fails |
403 insufficient_scope | Token lacks the scope; the scope field names it | Re-authorize with that scope |
403 permission_denied | Scope is fine — the user's role lacks the permission | Their admin must change their role |
400 on /token | Bad or expired code, PKCE mismatch, or wrong redirect_uri | Restart the flow |
That third row is the one people lose time on. See Scopes.
Security requirements
- PKCE (S256) is mandatory. No client secret exists to fall back on.
- Redirect URIs must be HTTPS, match a registered URI exactly, and their host must be allow-listed.
- Rotate refresh tokens — store the newest after every refresh.
- Least privilege — request the minimum scopes; a user who sees a smaller consent screen is a user who says yes.