Skip to main content

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 URLhttps://api.account.telebroad.com
Discovery/.well-known/oauth-authorization-server
PKCERequired, S256
Access token~1 hour
Refresh tokenRotates on every use

The flow

  1. Discover the endpoints — once.
  2. Register your app — once.
  3. Authorize — send the user to consent.
  4. Exchange the code for tokens.
  5. 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:

  • https only for internet hosts. The scheme is checked before the host list, so http://app.acme.example/callback is 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_uri must be character-for-character one of the values you registered — a trailing slash or an http/https swap is a redirect_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

StatusMeaningAction
401Missing or expired tokenRefresh, then re-authorize if that fails
403 insufficient_scopeToken lacks the scope; the scope field names itRe-authorize with that scope
403 permission_deniedScope is fine — the user's role lacks the permissionTheir admin must change their role
400 on /tokenBad or expired code, PKCE mismatch, or wrong redirect_uriRestart 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.