Twitch (twitch)
Twitch is a live-streaming platform. Rustiz does not create streams or clips:
a “post” is a chat message in the broadcaster’s own channel, or a
/announcement highlighted in that chat. Threads become replies to the previous
chat message.
- Rust source:
crates/rustiz-providers/src/twitch.rs - Upstream port:
twitch.provider.ts - Editor: Normal
- Connection mode:
oauth - No sandbox and no app review for ordinary chat apps.
Overview
| Property | Value |
|---|---|
| Identifier | twitch |
| Display name | Twitch |
| Connection mode | oauth (authorization code grant) |
| Editor | normal |
| Channel kind | social |
| Comments / threads | Yes. supports_comments = true; each extra entry is a chat reply (reply_parent_message_id) |
| Pending / async publishing | No. supports_pending = false; post() sends and returns Posted/Success |
| Analytics | No |
| Post analytics | No |
| Mentions | No |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 5 |
Character limit (max_length) | 500 characters, hard-coded |
| OAuth scopes | user:write:chat, user:read:chat, moderator:manage:announcements |
| Redirect URI | {FRONTEND_URL}/integrations/social/twitch |
Server Environment Variables
required_env() returns exactly:
#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
&["TWITCH_CLIENT_ID", "TWITCH_CLIENT_SECRET"]
}
}
required_env_alternatives() is not overridden.
| Variable | Required | Default | Purpose |
|---|---|---|---|
TWITCH_CLIENT_ID | Yes | — | Client id from the Twitch developer console; also sent as the Client-Id header on every Helix call |
TWITCH_CLIENT_SECRET | Yes | — | Client secret; used for the authorization-code and refresh-token exchanges |
TWITCH_ID_BASE_URL | No | https://id.twitch.tv | OAuth origin (/oauth2/authorize, /oauth2/token). Read through Config::base_url("TWITCH_ID", …). |
TWITCH_BASE_URL | No | https://api.twitch.tv | Helix origin (/helix/users, /helix/chat/...). Read through Config::base_url("TWITCH", …). |
FRONTEND_URL | Yes in practice | http://localhost:5000 | Base of the redirect URI |
JWT_SECRET | No direct use | empty | Not read by the Twitch flow (the OAuth state row has no encrypted client info). The rest of the server uses it for other secrets. |
rustiz providers --status reports twitch as configured only when both
client values are non-empty. It cannot verify that the secret is current;
Twitch invalidates the previous secret when you click New Secret.
Step-by-Step Developer App Setup
1. Prepare the Twitch account
- Sign in at https://dev.twitch.tv/console with the Twitch account that will own the app.
- Verify the account’s email.
- Enable two-factor authentication at https://www.twitch.tv/settings/security. Twitch requires 2FA to use the developer console. Refresh the console after enabling it.
- Open Applications → Register Your Application.
2. Register the app
| Field | Value |
|---|---|
| Name | Any unique name, e.g. Rustiz |
| OAuth Redirect URLs | https://social.example.com/integrations/social/twitch — then click Add |
| Category | Choose the closest category (e.g. Chat Bot / Website) |
| Client type | Confidential / server-side (Rustiz stores a client secret) |
| CAPTCHA | Complete it |
After Create, open Manage and copy the Client ID. Click New Secret and copy the secret once; Twitch will not show it again.
Twitch allows multiple redirect URLs on one app. Add every deployment (production, staging, local) that will use the same client id. There is no product or API subscription to enable in the console; the app may call Helix immediately with the scopes the user grants.
3. Set the environment and restart
TWITCH_CLIENT_ID="…"
TWITCH_CLIENT_SECRET="…"
Restart rustiz serve, rustiz worker and rustiz mcp. Check:
rustiz providers --status
4. Redirect URI (exact)
Twitch::redirect_uri(None) is:
{FRONTEND_URL}/integrations/social/twitch
FRONTEND_URLis trimmed of trailing slashes, so register it without a trailing slash.- There is no
redirectmeto.comwrapper for Twitch. - Local example:
http://localhost:5000/integrations/social/twitch. Twitch acceptshttp://localhostredirects for development; for a public deployment use HTTPS. - The same URI is sent to
/oauth2/authorizeand/oauth2/token. Twitch requires them to match the registered value.
There is a refresh quirk: when a connection is started with a refresh
query parameter, authenticate() sends redirect_uri as
{FRONTEND_URL}/integrations/social/twitch?refresh=<internal-id> while
generate_auth_url() still sends the URI without the query string. Twitch
matches redirect URIs exactly, so a token exchange during a “reconnect/refresh”
flow can fail with invalid redirect_uri. Normal CLI/MCP connections do not set
refresh, so they are unaffected.
5. Scopes requested and checked
Twitch::new() sets:
#![allow(unused)]
fn main() {
info.scopes = vec![
"user:write:chat",
"user:read:chat",
"moderator:manage:announcements",
];
}
| Scope | Used by | Why |
|---|---|---|
user:write:chat | POST /helix/chat/messages | Send chat messages as the authenticated user |
user:read:chat | Nothing in this provider | EventSub channel.chat.message normally needs it; Rustiz never subscribes. It is an extra scope. |
moderator:manage:announcements | POST /helix/chat/announcements | Send highlighted announcements to the broadcaster’s chat |
The authorize URL URL-encodes the space-joined list:
https://id.twitch.tv/oauth2/authorize?response_type=code
&client_id=…
&redirect_uri=…
&scope=user%3Awrite%3Achat%20user%3Aread%3Achat%20moderator%3Amanage%3Aannouncements
&state=…
check_scopes() is never called. The port does not read the scope array
returned by the token exchange, so a user who unchecks a permission still gets
a “connected” channel; the failing call appears later.
Twitch’s own guidance says an app must request only the scopes it uses, and may
suspend access for over-requesting. user:read:chat is not used by Rustiz;
removing it from info.scopes is safe for this provider and reduces review
risk.
6. Sandbox / testing vs production
- No sandbox. Twitch has no test-mode app. The app works against the production API as soon as it is created.
- No app review for ordinary chat/Helix apps. The account owner can authorize the app immediately; the consent screen lists the scopes.
- Test with your own Twitch channel: authorize the app with the broadcaster account, then send a message or an announcement. The message is visible to real viewers if the channel is live.
- Use a separate Twitch account/channel for tests if you do not want test messages in a real chat.
- Production has the same setup. Twitch may still suspend an app for scope misuse, spam or API terms violations; there is no pre-approval step that prevents that.
7. Token lifetime and validation
- Authorization-code tokens include a refresh token.
authenticate()storesexpires_infrom the token response (Twitch access tokens are typically valid for about 4 hours; the exact value is in the response). refresh_token()performs the refresh grant and fetches/helix/usersagain, so the stored id/name/avatar stay current.- Twitch’s docs require third-party apps that maintain an OAuth session to call
GET /oauth2/validateperiodically. Rustiz does not call/validate. It relies on API 401s to trigger the refresh path. - A new client secret invalidates the old one; update
TWITCH_CLIENT_SECRETand reconnect if refreshes start failing.
Connecting a Channel (CLI & MCP)
CLI
rustiz channels connect twitch --open
- Prints the Twitch consent URL;
--openopens it. - With a local
FRONTEND_URL, the CLI serves the callback on the frontend port and waits up to--timeout(default 300 s). - With a public HTTPS
FRONTEND_URL, the CLI prints the URL and exits; the server handles the callback. Pollrustiz channels connect-status <id>orrustiz channels list. - No
--external-urland no--fields: Twitch is a normal OAuth provider. PassingcustomFieldsroutes toconnect_custom(), which encodes the object as an authorization code and fails. Do not send it.
MCP connectChannelTool
{ "provider": "twitch" }
Returns:
{
"connected": false,
"pending": true,
"requiresHuman": true,
"connectionId": "…",
"url": "https://id.twitch.tv/oauth2/authorize?...",
"message": "A human must open this URL and log in. Then poll with connectionId or check integrationList."
}
Poll with:
{ "connectionId": "…" }
integrationSchema reports:
{ "type": "oauth", "headless": false, "requiresHuman": true, "requiresExternalUrl": false }
There is no triggerTool / channelSetupTool follow-up: the broadcaster is
the authenticated user, so is_between_steps is false.
What gets stored
GET /helix/users returns the user’s id, display_name, profile_image_url
and login. AuthTokenDetails stores:
id= Twitch user id (used asbroadcaster_idandsender_id)name= display nameusername= loginaccess_token,refresh_token,expires_inpicture= profile image URL
If /helix/users returns an empty data array, the channel can still be saved
with empty id/name because save() only requires a non-empty access token.
That channel will fail every publish with a missing/invalid broadcaster id.
Post Settings & Media Validation
settings_schema()
{
"type": "object",
"properties": {
"messageType": {
"type": "string",
"enum": ["message", "announcement"],
"description": "Publish a normal chat message or a channel announcement (default message)"
},
"announcementColor": {
"type": "string",
"enum": ["primary", "blue", "green", "orange", "purple"],
"description": "Color of the announcement, only used when messageType is announcement (default primary)"
}
},
"additionalProperties": true
}
Defaults when omitted: messageType = "message", announcementColor = "primary".
validate_settings() rules
- If
messageTypeis present and notnull, it must be exactly"message"or"announcement". Any other string, number or object fails withmessageType must be message or announcement. - If
announcementColoris present and notnull, it must be exactly"primary","blue","green","orange"or"purple". Otherwise it fails withannouncementColor must be primary, blue, green, orange or purple. - A
nullvalue is accepted and falls back to the default at publish time. - Extra properties are allowed.
The same settings are read for comments, so a comment can be an announcement too.
check_validity()
Not overridden. There are no pre-flight media rules.
Media requirements
Media is ignored. Twitch chat cannot attach images or video. post() and
comment() never read post.media; a post with images publishes as text only,
with no warning. This is a silent data loss, not an error.
Message length
max_length()returns500.post()andcomment()also truncate to 500charsbefore sending (truncate_chars).- The engine counts characters after HTML stripping and rejects a longer post before it reaches Twitch; the provider truncation is a second guard.
- Twitch’s docs say the message is limited to 500 characters. Emote names are sent as plain text and Twitch converts recognized names; Rustiz does not add colons.
Chat message vs announcement
Normal message
POST {TWITCH_BASE_URL}/helix/chat/messages
Authorization: Bearer <user token>
Client-Id: <TWITCH_CLIENT_ID>
Content-Type: application/json
{
"broadcaster_id": "<integration.internal_id>",
"sender_id": "<integration.internal_id>",
"message": "<truncated text>",
"reply_parent_message_id": "<optional, comments only>"
}
broadcaster_idandsender_idare both the authenticated user’s id. This posts to the user’s own channel only; Rustiz cannot post to someone else’s channel.- Twitch requires
sender_idto match the user in the access token. - The response is
{ data: [{ message_id, is_sent, drop_reason? }] }. - If
is_sentisfalse, Rustiz maps it toPostStatus::Success(the Rust enum has noerrorstate). The drop reason is discarded. A dropped message looks successful. - If the response has no
message_id, Rustiz generates a random 10-char id.
Announcement
POST {TWITCH_BASE_URL}/helix/chat/announcements
?broadcaster_id=<id>&moderator_id=<id>
Authorization: Bearer <user token>
Client-Id: <TWITCH_CLIENT_ID>
Content-Type: application/json
{ "message": "<truncated text>", "color": "primary|blue|green|orange|purple" }
broadcaster_idandmoderator_idare both the authenticated user’s id. The user must be the broadcaster or a moderator.- Twitch returns 204 No Content on success. The provider uses
execute()(notsend_json) because the retrying client would treat 204 as an error. - Any non-2xx is converted to
bad_bodywith the fixed messageUnknown Error. - Announcements do not return a message id, so Rustiz generates a random
10-char
post_id. - Twitch rate-limits announcements to one every 2 seconds per channel. The provider sleeps 2 seconds before every post/comment, which is a rough match but not a strict scheduler.
Threads
supports_comments = true:
- The first entry is a normal message or announcement.
- Each later entry sets
reply_parent_message_idtolast_comment_idwhen present, otherwise to the rootpost_id. - For a normal message, this creates a Twitch chat reply.
- For an announcement, the code takes the announcement branch and ignores
reply_parent_message_id; a thread of announcements becomes independent announcements, not replies. - Twitch replies are only one level deep in the UI; there is no root/thread metadata.
Plugs
No @Plug / @PostPlug methods. run_plug() and run_post_plug() return
Unsupported.
Implementation Notes & Gotchas
user:read:chatis unused. Rustiz never subscribes to EventSub or reads chat. Twitch may suspend an app that requests scopes it does not need. Remove it frominfo.scopesif you control the deployment and want the minimal scope set.- No
channel:bot/user:bot. The app sends as the authenticated user, not as a bot. This is fine for posting to the user’s own channel; it cannot join another channel’s chat as a bot. - Refresh redirect mismatch. As described above, a reconnect started with
?refresh=builds a token-exchangeredirect_urithat includes the query string while the authorize request did not. Twitch’s exact-match rule can reject it. Normal connections are unaffected. - No PKCE. Twitch’s authorization-code flow uses the client secret, and Rustiz sends it in the token form. That is correct for a confidential client, but it means the secret must be protected.
Authorization: Beareron Helix, notOAuth. Twitch accepts both for Helix, but the code usesBearerand always sendsClient-Id.- Token refresh does not send
scope. Twitch’s refresh grant does not require it; the new token keeps the originally granted scopes. - No
/oauth2/validate. Twitch’s docs require third-party apps to validate tokens periodically. Rustiz waits for an API 401 instead. A token can be revoked while the channel still looks healthy in Rustiz. - A 401 becomes a refresh error. The generic HTTP layer classifies an
unhandled 401 as
RefreshToken; the engine callsrefresh_token(). If the refresh token is also invalid, the channel is marked for reconnect. - No error classification.
handle_errors()returnsNone; Twitch’s JSON error bodies are passed through the generic layer.send_announcement()replaces every non-2xx withUnknown Error, losing Twitch’smessage. is_sent: falseis hidden. A message dropped by AutoMod, a slow mode, a ban or a follower-only mode is reported asSuccess, not an error. Thedrop_reasonobject is discarded. Check Twitch chat if a post claims success but does not appear.- Announcements have a random post id.
post_idismake_id(10), not a Twitch identifier. The release URL is just the channel URL, so post analytics/deletion cannot target the announcement. - Release URL is the channel, not the message.
https://twitch.tv/{login}(orprovider_identifierifprofileis empty). Twitch chat messages have no stable public permalink. A missingprofileproduceshttps://twitch.tv/twitch, which is wrong. - 2-second sleep on every send.
post()andcomment()sleep 2 seconds before doing anything. This throttles bursts but also adds latency. Combined withmax_concurrent_job = 5, a queue can still send several messages at once; the sleep is not a global lock. - Rate limits. Twitch Helix uses a points bucket (commonly 800 points per
minute per user token) and returns
429withRatelimit-*headers. Chat sends have their own limits; announcements are documented as one per 2 seconds. The generic HTTP layer retries429/5xxup to three times with the configured delay; the 2-second sleep reduces but does not eliminate 429s. - Media is silently dropped. Twitch chat has no attachments. A post with
images publishes as text;
check_validity()does not warn. max_lengthis 500 Unicode scalar values. Twitch’s docs say the message is limited to 500 characters. Rust countschars, so a grapheme cluster made of several code points (many emoji, flags, skin-tone sequences) consumes more than one unit and can be truncated earlier than the Twitch UI would.- The app is for one user.
broadcaster_id/sender_idare always the authenticated user. To post to multiple channels, connect each broadcaster account as a separate channel; Rustiz cannot target arbitrary channels with one token. - No analytics or mentions. Both flags are
false; there are no overrides.
References
- Twitch developer console: https://dev.twitch.tv/console/apps
- Registering an app: https://dev.twitch.tv/docs/authentication/register-app/
- OAuth flows and token exchange: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/
- Scopes: https://dev.twitch.tv/docs/authentication/scopes/
- Send Chat Message: https://dev.twitch.tv/docs/api/reference/#send-chat-message
- Send Chat Announcement (one per 2 seconds): https://dev.twitch.tv/docs/api/reference/#send-chat-announcement
- Refreshing access tokens: https://dev.twitch.tv/docs/authentication/refresh-tokens/
- Validating tokens: https://dev.twitch.tv/docs/authentication/validate-tokens/
- Postiz Twitch settings: https://docs.postiz.com/public-api/providers/twitch