Skip to content
TipPage Docs
Esc
navigateopen⌘Jpreview
On this page

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.

API keys are for your own TipPage. OAuth is for when you build something other streamers use: instead of asking each of them to create a key and paste it into your app, you give them a “Connect TipPage” button. They approve a consent screen on auth.tippage.com, and your app receives an access token that works everywhere an API key does - every /v1 endpoint, the realtime stream, the same scopes, the same rate limits.

It is standard OAuth 2.0: authorization code grant, PKCE (S256), refresh tokens that rotate on use, RFC 8414 server metadata. Any OAuth client library works.

Authorization endpoint https://api.tippage.com/oauth/authorize
Token endpoint https://api.tippage.com/oauth/token
Revocation endpoint https://api.tippage.com/oauth/revoke
Server metadata https://api.tippage.com/.well-known/oauth-authorization-server
Access token lifetime 1 hour
Refresh token lifetime Never expires - rotated on every refresh, ends only when revoked

1. Register your app

Dashboard -> Settings -> Developer -> OAuth apps -> Register app (owner and super admins only). You pick:

  • Name and description - shown to streamers on the consent screen, next to your TipPage account name. Names that could pass for TipPage itself or a platform (Twitch, Kick, Stripe, …) are refused. Website is shown only in the streamer’s Connected apps list, as an unverified claim - the consent screen never renders links you control, only the host of your redirect_uri, because that’s the one thing TipPage can vouch for. Apps TipPage ships itself carry an Official TipPage app badge on the consent screen instead; that flag is set by TipPage staff and can’t be requested through the dashboard.
  • Redirect URIs - exact-match, one per line. https:// only, except http://localhost (any port) for development and reverse-domain custom schemes (com.example.app:/callback) for native apps. No wildcards, no fragments.
  • Scopes - the most your app may ever ask for. Each authorization requests a subset. The scope list is the same one API keys use.
  • Client type - confidential (server-side; gets a client secret, shown once, rotatable) or public (desktop, mobile, browser; no secret, PKCE required on every authorization).

You get a client_id (oc_...) and, for confidential clients, a client_secret (tpcs_...). Up to 10 apps per account.

PKCE in thirty seconds

PKCE (RFC 7636) stops a stolen authorization code from being useful. Before you send the streamer off, you make up a secret (the verifier) and send only its hash (the challenge) with the authorize request. When you exchange the code you send the original verifier, and TipPage checks it hashes to the challenge it saw. Someone who intercepts the code (a leaky redirect, a malicious app on the same device) doesn’t have the verifier and can’t redeem it.

  • Required for public clients - anything without a client secret.
  • Recommended for confidential clients too; TipPage verifies it whenever a challenge was sent.
  • Only S256 is accepted (plain is refused).
  • Generate a fresh verifier per authorization and keep it with your state until the callback. Never reuse one.
code_verifier  = 43..128 chars from [A-Z a-z 0-9 - . _ ~]  (random)
code_challenge = BASE64URL( SHA256( code_verifier ) )       (no padding)
// node
import crypto from "node:crypto";
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
# python
import base64, hashlib, secrets
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
// browser
const bytes = crypto.getRandomValues(new Uint8Array(32));
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const verifier = b64url(bytes);
const challenge = b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));

Send code_challenge=<challenge>&code_challenge_method=S256 in step 2 and code_verifier=<verifier> in step 3. A mismatch fails the exchange with invalid_grant: PKCE verification failed. Most OAuth client libraries do all of this for you when you pass code_challenge_method: "S256".

2. Send the streamer to authorize

https://api.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, verified on return>
  &code_challenge=<BASE64URL(SHA256(code_verifier))>
  &code_challenge_method=S256
Parameter
response_type Always code
client_id Your app’s id
redirect_uri Must exactly match a registered URI. Optional only if the app has exactly one
scope Space-separated, a subset of the app’s registered scopes
state Recommended - opaque value you check when the user comes back (CSRF)
code_challenge + code_challenge_method=S256 See PKCE. Required for public clients, recommended for all. Only S256 is accepted

TipPage validates the request, then shows the consent screen on auth.tippage.com. The streamer signs in if they aren’t already (Twitch or Kick), sees your app’s name, description and the TipPage account it belongs to, exactly the scopes you asked for, where they will be sent back to, and Authorize / Deny. Only the account owner or a super admin can authorize - the same rule as creating an API key.

Back at your redirect_uri:

  • Approved: ?code=<authorization code>&state=<your state> - the code is single-use and expires in 10 minutes.
  • Denied: ?error=access_denied&state=....
  • Anything wrong with the request itself (invalid_scope, unsupported_response_type, missing PKCE on a public client) is also returned as ?error= - except an unknown client_id or an unregistered redirect_uri, which are answered as a JSON 400 without redirecting, so a bad link can never bounce someone to an arbitrary site.

3. Exchange the code

POST /oauth/token with a form body (application/x-www-form-urlencoded; JSON is accepted too). Confidential clients authenticate with HTTP Basic (client_id:client_secret) or client_secret in the body; public clients send just client_id.

curl https://api.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 exactly like an API key:

curl https://api.tippage.com/v1/me -H "Authorization: Bearer tpat_..."

GET /v1/me tells you whose TipPage you’re acting on (tenant) and, for OAuth tokens, "credential": "oauth" plus an oauth block with your client_id and the authorization_id (oa_...).

4. Refresh

Access tokens last an hour. Before (or when) one expires:

curl https://api.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; the old refresh token is dead the moment the new one is issued. Store the new pair. Refresh tokens don’t expire on their own: a streamer who connects your app stays connected until they revoke it in Settings -> Developer -> Connected apps, or your app calls /oauth/revoke. There’s no periodic re-consent. If a rotated-out refresh token is ever presented again, TipPage treats it as a leaked copy and revokes the whole authorization - the streamer has to reconnect.

Revocation

Either side can end it:

  • The streamer: Settings -> Developer -> Connected apps -> Revoke, or by disconnecting your app after you disable/delete it. Every token dies and any live realtime socket is closed within seconds.
  • Your app: POST /oauth/revoke with token=<access or refresh token> and your client credentials (RFC 7009). Revoking either kind ends the whole authorization. Unknown tokens still return 200.

Rotating your client secret does not revoke anything - existing tokens keep working, but the next refresh needs the new secret. Removing a scope from your app’s registration revokes every current connection (they hold more than the app may now ask for); adding scopes or changing redirect URIs keeps them, and a re-authorization replaces the previous one (one live session per app per streamer).

The realtime stream

Connect to wss://ws.tippage.com and send {"data":{"apiKey":"tpat_..."}} as the first message - the apiKey field takes an access token too. Channels are derived from the authorization’s scopes; the socket stays up while the authorization lives (revocation closes it), not just while that particular access token is valid. POST /v1/realtime/token handoff tokens work the same as with a key. See Realtime.

What events say about you

Everything your app does through the API carries an actor of { "type": "oauth_app", "id": "oc_...", "name": "<your app>", "authorization_id": "oa_..." } on webhooks, the realtime stream and the streamer’s dashboard (“TTS paused by Your App”). See the actor table in Webhooks.

Errors

The token endpoint uses the standard shape: { "error": "...", "error_description": "..." }. invalid_client is a 401; invalid_grant (bad/expired/reused code or refresh token, PKCE mismatch, redirect_uri mismatch), unsupported_grant_type and invalid_scope are 400s. A /v1 call with an expired or revoked access token gets 401 { "code": "invalid_token" } - refresh and retry, or send the streamer through authorize again if the refresh fails with invalid_grant.

Limits and safety

  • Scopes never include payments, settings or team management - the same ceiling as API keys.
  • Tokens and secrets are stored hashed; a secret is shown once at creation or rotation.
  • Rate limits are per authorization (120 requests/min), same as per key.
  • Authorize requests and codes live 10 minutes and are single-use.
  • Don’t put a confidential client’s secret in a browser or a shipped binary - register a public client and use PKCE instead.

Was this page helpful?