Sign in with itzon
An OAuth 2.0 authorization code flow that lets your app verify a user's itzon identity and, with the api scopes, call the Public API on the user's behalf.
Registering your app#
- Clients are registered by site staff; there is no self-service form. Reach out on the Discord linked in the navbar with your app name (max 64 characters), up to 8 redirect URIs, and whether the client is confidential or public.
- Redirect URIs must be absolute
http(s)URLs without a fragment, max 512 characters, and are matched exactly, character for character, including any query string. The one exception is loopback: a registeredhttp://127.0.0.1:<any port>/pathorhttp://[::1]:<any port>/pathURI matches on any port (RFC 8252), so a desktop app can bind an ephemeral port for its callback listener. - A confidential client (server-side app) receives a 32-hex
client_idand a 64-hexclient_secret. The secret is shown once; store it server-side and never ship it to a browser or app binary. - A public client (desktop app, open-source client, SPA; anything that cannot keep a secret) receives only a
client_idand authenticates every code exchange with PKCE instead. See the Public clients section below.
Scopes#
Scopes are space-separated and shown to the user on the consent page. identity is always implied: requesting api:read grants identity api:read. Anything outside this table is rejected with invalid_scope.
| Scope | Grants |
|---|---|
identity | confirm who the user is: GET /api/oauth/userinfo (public username, user id, avatar). Every token has it. |
chat | connect to chat as the user: the access token works as the IRC PASS and the connection gets the user's own name, badges and roles |
api:read | the Public API read endpoints: channel, channel followers, categories, me, me/followers |
api:write | PUT /api/public/v1/me/stream-info only; implies nothing else |
Access tokens share their token store and semantics with the personal API tokens created in the dashboard Settings tab: the Public API treats both kinds identically and only scopes decide access. The difference is lifetime: an app token expires after 1 hour and is renewed with its refresh token, a personal token never expires until its owner deletes it.
1. Send the user to the consent page#
https://itzon.tv/oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&response_type=code
&scope=identity
&state=RANDOM_STRING
| Param | Rules |
|---|---|
client_id | required, your 32-hex id |
redirect_uri | required, must exactly match a registered URI |
response_type | required, exactly code |
scope | space-separated scopes from the table above; omitted defaults to identity |
state | optional, max 512 characters, echoed back untouched; use a random value to tie the callback to the session that started it |
code_challenge | PKCE challenge, base64url of SHA-256 over your code_verifier (43 characters, no padding); required for public clients, optional for confidential ones |
code_challenge_method | exactly S256 whenever code_challenge is sent; plain is not supported |
A signed-out user is sent through login first and lands back on the consent page. The page shows your app name and a plain-language line per requested scope with Approve and Deny buttons.
2. Receive the redirect#
Approval navigates to your redirect URI with a code:
https://app.example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_STRING
Denial redirects with error=access_denied and the same state. The code is single use and expires 60 seconds after it is minted, so exchange it immediately.
3. Exchange the code for a token#
curl -X POST https://itzon.tv/api/oauth/token \
-d grant_type=authorization_code \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code=AUTHORIZATION_CODE \
-d redirect_uri=https://app.example.com/callback
{
"access_token": "64_HEX_TOKEN",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "64_HEX_TOKEN",
"refresh_expires_in": 2592000,
"scope": "identity"
}
Form-encoded and JSON bodies are both accepted. redirect_uri must be the exact value used in step 1. If the code was minted with a code_challenge, the exchange must also carry the matching code_verifier. Access tokens live for 1 hour; use the refresh token to get a new pair when one expires.
Public clients (PKCE)#
A public client holds no client_secret and proves itself with PKCE (RFC 7636) instead,
so an open-source desktop app or SPA can ship its client_id in the clear. Per
authorization: generate a random code_verifier (43-128 characters of
A-Z a-z 0-9 - . _ ~), send code_challenge=BASE64URL(SHA256(code_verifier))
with code_challenge_method=S256 in step 1, then exchange the code with the verifier
and NO secret:
curl -X POST https://itzon.tv/api/oauth/token \
-d grant_type=authorization_code \
-d client_id=YOUR_CLIENT_ID \
-d code=AUTHORIZATION_CODE \
-d code_verifier=YOUR_CODE_VERIFIER \
-d redirect_uri=http://127.0.0.1:49152/callback
Rules: the challenge is REQUIRED for a public client and rejected as invalid_request
when missing or not S256; sending a client_secret from a public client
answers invalid_client; a wrong or missing code_verifier answers
invalid_grant and the code is burned. The refresh grant needs only
client_id and the refresh token; rotation and reuse detection protect it the same
way as for confidential clients. Desktop apps should register a loopback redirect URI and use
whatever free port they bound at runtime, see Registering above.
Connecting to chat#
An access token whose scope includes chat doubles as an IRC credential: open the
chat connection and send PASS ACCESS_TOKEN as the first
command. The connection is assigned the approving user's own username, badges and roles, exactly
as if they had signed in on the site. When the access token expires (1 hour) the server revokes
the chat session within its 90 second revalidation window (TCP kill / WS close 4403);
refresh the token and reconnect with the new one.
Refreshing a token#
curl -X POST https://itzon.tv/api/oauth/token \
-d grant_type=refresh_token \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d refresh_token=REFRESH_TOKEN
The response has the same shape as the code exchange. Refresh tokens live for 30 days, are single use,
and rotate: a successful refresh revokes BOTH the old access token and the old refresh token atomically
and issues a fresh pair, so store the new refresh_token from every response. Reusing a
consumed refresh token, presenting it with a different client, or refreshing after the 30 days answer
invalid_grant; the user then has to run the authorization flow again.
4. Fetch the user's identity#
curl https://itzon.tv/api/oauth/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"
{
"id": 42,
"username": "streamer",
"avatar": "https://itzon.tv/api/live/profile/streamer/avatar?v=1722880000000"
}
avatar is null when the user has not uploaded one.
Error codes#
Failures answer with OAuth-style JSON: {"error": "...", "error_description": "..."}.
| Code | Where | Meaning |
|---|---|---|
invalid_request | authorize, token | missing or malformed parameter, or an unregistered redirect URI |
invalid_client | authorize, token | unknown client_id or wrong client_secret |
unsupported_response_type | authorize | response_type is not code |
invalid_scope | authorize | a scope outside identity, chat, api:read, api:write |
unsupported_grant_type | token | grant_type is not authorization_code or refresh_token |
invalid_grant | token | code or refresh token invalid, expired, already used, or bound to a different client (codes also check the redirect URI and, when PKCE started the flow, the code_verifier) |
invalid_token | userinfo | access token invalid or expired |
login_required | authorize, consent | no signed-in session; the consent page handles this by redirecting to login |
access_denied | redirect | the user pressed Deny |
temporarily_unavailable | authorize, token | rate limited (30 requests per minute per IP), retry later |