Twitch Accepts Invalid Keys and Silently Discards Them — The Hidden Pitfalls of Integration and a 41-Second Recovery A developer building an AI avatar live streaming system on Twitch encountered several integration pitfalls, including Twitch silently accepting invalid stream keys and OAuth authorization codes expiring within minutes. The developer implemented a 41-second recovery process and shared lessons on configuring OAuth scopes, using force_verify, and validating tokens to ensure unattended operation. 📝 Originally published in Japanese at forge.workstyle.tech . When building an AI avatar live streaming system that runs unattended, the development focus shifts from a typical application. If the system crashes during unmonitored hours, no one is there to fix it. This means that instead of just "working well," the key to quality is making sure the system "understands when it breaks and can recover on its own." This article summarizes the pitfalls encountered while getting such a system to run continuously on Twitch, broken down into three layers: ffmpeg is sending video, the channel doesn’t go live. Twitch silently accepts invalid keys.All of these issues share a common pattern: "Everything looks normal from the sender’s side." This makes them especially tricky. Let’s go through them one by one. First, to just read chat, an anonymous IRC connection is sufficient—no app registration required. That part was straightforward. OAuth became necessary when we wanted to do two things: Only these two operations require authorization. The overall flow looks like this: 1. Register an app in the Developer Console Confidential type 2. Obtain client id / client secret 3. Open the authorization URL in a browser and authorize as the channel owner 4. Extract the authorization code code from the redirect URL 5. Exchange the code for an access token and refresh token via the token endpoint 6. Save the refresh token on the server Only steps 3 and 4 require human interaction; the rest can be automated. Below are the specific pain points we encountered during this process. When registering the app, you must select a type. If your server holds the client secret and performs token exchange, choose Confidential . Choosing the wrong type later means the secret can’t be used for token exchange, and you’ll have to recreate the app. The redirect URI is only needed to complete authorization, so something like http://localhost:3000 is fine. You don’t even need to run a server to receive it— just copy the code from the address bar after authorization. If scopes are missing, you’ll have to restart the authorization process. The three scopes we needed were: | Scope | Purpose | |---|---| bits:read | Subscribe to bits cheers events | channel:read:subscriptions | Subscribe to subscription events | channel:manage:broadcast | Set stream metadata like the title | Adding scopes later requires the user to open a browser again. List all required functionality upfront and request all necessary scopes in one authorization. force verify to Avoid Authorizing the Wrong Account We added force verify=true to the authorization URL. Without it, if you’re already logged into the browser, authorization might complete without a confirmation screen , especially if you have both a personal account and a character account. This can lead to accidentally authorizing the wrong account. With force verify=true , a confirmation screen always appears, making it clear which account you’re authorizing. This was the most nerve-wracking issue. Authorization codes code expire in just a few minutes. If the flow involves human steps—opening a browser, copying the code, passing it to the server, and exchanging it—the code may already be dead by the time you receive it. The fix is simple: prepare the token exchange process in advance and execute it the moment the code arrives. We pre-assembled the exchange command and waited. As soon as the code came in, we executed it immediately, and it worked on the first try. Once you get the token, hit the validation endpoint /oauth2/validate to inspect its contents. curl -H "Authorization: OAuth