OAuth 2.0
Let other streamers connect your app to their TipPage with a Connect button - authorization code + PKCE, refresh tokens, and the same scopes as API keys.
For apps that other streamers use. They approve a consent screen and your app gets an access token that works everywhere an API key does - every /v1 endpoint, the realtime stream, the same scopes and rate limits. Standard OAuth 2.0: authorization code + PKCE (S256), rotating refresh tokens, RFC 8414 metadata. Any OAuth library works.
| Authorize | https://auth.tippage.com/oauth/authorize |
| Token | https://auth.tippage.com/oauth/token |
| Revoke | https://auth.tippage.com/oauth/revoke |
| Metadata | https://auth.tippage.com/.well-known/oauth-authorization-server |
| Access token | 1 hour |
| Refresh token | Never expires; rotated on every use, ends only when revoked |
The older api.tippage.com/oauth/token and /oauth/revoke still work.
1. Register your app
Settings -> Developer -> OAuth apps (owner and super admins, up to 10 apps):
- Name and description - shown on the consent screen. Names that could pass for TipPage or a platform are refused.
- Redirect URIs - exact match.
https://, orhttp://localhost(any port), or a reverse-domain scheme (com.example.app:/callback). No wildcards. - Scopes - the most the app may ask for; each authorization requests a subset of the same scopes keys use.
- Client type - confidential (server-side, gets a
tpcs_secret) or public (desktop/mobile/browser, no secret, PKCE required).
You get a client_id (oc_...).
2. Send the streamer to authorize
https://auth.tippage.com/oauth/authorize
?response_type=code
&client_id=oc_...
&redirect_uri=https://yourapp.example/oauth/callback
&scope=tts:read%20tts:control%20chat:write
&state=<random>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256
redirect_uri can be left out only when the app has exactly one. PKCE is required for public clients and checked whenever a challenge is sent; only S256 is accepted. Make a fresh verifier per authorization:
import crypto from "node:crypto";
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
Only the account owner or a super admin can approve. They come back to your redirect_uri with ?code=...&state=... (single use, 10 minutes) or ?error=access_denied. Other request errors also come back as ?error=, except an unknown client_id or unregistered redirect_uri, which are shown on the consent page and never redirected.
3. Exchange the code
Form body (JSON also accepted). Confidential clients authenticate with HTTP Basic or client_secret in the body; public clients send client_id.
curl https://auth.tippage.com/oauth/token -u "oc_...:tpcs_..." \
-d grant_type=authorization_code -d code=... \
-d redirect_uri=https://yourapp.example/oauth/callback -d code_verifier=...
{ "access_token": "tpat_...", "token_type": "Bearer", "expires_in": 3600,
"refresh_token": "tprt_...", "scope": "tts:read tts:control chat:write" }
Use the access token like a key. GET /v1/me says whose TipPage it is, credential.type: "oauth", the granted scopes, and your app in oauth.
4. Refresh
curl https://auth.tippage.com/oauth/token -u "oc_...:tpcs_..." \
-d grant_type=refresh_token -d refresh_token=tprt_...
You get a new access token and a new refresh token; store both, the old refresh token is dead. Presenting a rotated-out refresh token again revokes the whole authorization (it’s treated as leaked).
Revocation
- The streamer: Settings -> Developer -> Connected apps -> Revoke. Tokens die and realtime sockets close within seconds.
- Your app:
POST /oauth/revokewithtoken=<access or refresh token>and your client credentials (RFC 7009). Either token ends the whole authorization. Unknown tokens still return 200.
Rotating your client secret revokes nothing. Removing a scope from the app’s registration revokes every connection; adding scopes or redirect URIs doesn’t. Re-authorizing replaces the previous connection (one per app per streamer). Official TipPage apps are the exception: each approval is its own connection, so the OBS dock can be signed in on two machines at once, and each shows as its own row to revoke.
Everything else
- Realtime: send the access token as
apiKeyon the stream. The socket lives as long as the authorization, not the access token. - Events your app causes carry
actor: { "type": "oauth_app", "id": "oc_...", "name", "authorization_id": "oa_..." }. - Errors:
{ "error", "error_description" }.invalid_clientis 401;invalid_grant,invalid_scope,unsupported_grant_typeare 400. A/v1call with a dead access token gets401 invalid_token- refresh, and if that fails withinvalid_grant, send the streamer through authorize again. - Never put a confidential client’s secret in a browser or a shipped binary - register a public client.