Extension Attempting to Send Messages In Chat Returns 401 "authentication failed" with a valid app access token

Hello, I’m having issues with my extension sending messages in chat. Whenever it attempts to send a message I get the error 401 “authentication failed” in my EBS server even though I appear to have a valid app access token

Extension client ID: 3v5bihl15jwi01ord0tsrxtg49tk7h (Aquarium Game)
Versions tested: 1.0 (released) and 1.0.1 — both return 401
Broadcaster tested: channel 1530964356, extension ACTIVATED (video overlay) and channel LIVE during tests

Preconditions verified:

  • Chat Capabilities enabled on the extension (enabled since creation)
  • Broadcaster has granted the extension Chat permission in channel Extension settings
  • App access token mints successfully (client_credentials) and the SAME token returns
    HTTP 200 on GET /helix/users in the same test run
  • POST /helix/extensions/chat body used exactly the documented fields:
    broadcaster_id, extension_id, extension_version, text

Diagnostic output (from my EBS, one run, same token throughout):
appToken: OK
GET /helix/users?id=1530964356 → 200
POST /helix/extensions/chat version 1.0 → 401 {“error”:“Unauthorized”,“message”:“authentication failed”}
POST /helix/extensions/chat version 1.0.1 → 401 {“error”:“Unauthorized”,“message”:“authentication failed”}

Timeline: the identical 401 has occurred across prior extension versions during
hosted testing and continues on the released 1.0, over multiple weeks. Other
authenticated Helix calls from the same EBS (user lookups, extension transactions,
Send Extension PubSub Message via signed extension JWT) all succeed.

This matches long-standing reports on the developer forums of 401
“authentication failed” on this endpoint with valid tokens (e.g. threads 26686,
30384, 33052). I’m requesting investigation of my extension’s chat-send authorization
state on Twitch’s side. Also if I could receive conformation that an extensions approval status does not affect the chat authentication that would be helpful as well. I’d be happy to provide any additional identifiers needed.

This API doesn’t use App Access Tokens? It requires a JWT.

Sounds like you had the test version installed on the target channel and released it.

Full uninstall the extension and reinstall it per Permissions _just don't work_ if a newly release if the test version was installed · Issue #359 · twitchdev/issues · GitHub

Or

You’ve used the wrong token type.

Extension status (local/hosted/review/release) doesn’t effect the operation of the send chat endpoint.

–

Documentation for reference:

Authorization

Requires a signed JSON Web Token (JWT) created by an Extension Backend Service (EBS). For signing requirements, see Signing the JWT. The signed JWT must include the role and user_id fields (see JWT Schema). The role field must be set to external.

Thank you for your response. The token type was definitely an issue and has been resolved. However I still get similar results so I must be missing something. I’ve tested the chat capabilities with ‘Twitch.ext.features.isChatEnabled’ in the console on twitch while live and it has returned false every time but once (I have been unable to replicate the one time it was true) Here is a summary:

On the token type: you were right, we were using an app access token. We now sign an EBS extension JWT (HS256, base64url segments, secret base64-decoded before HMAC, role: external, plus user_id and channel_id, 30s expiry). That change is visible in the error: it moved from

textCopy

401 {"message":"authentication failed"}

to

textCopy

401 {"message":"The extension is not allowed to send a chat message to the specified broadcaster's chat room."}

Different failure mode. The app token was rejected as a credential; the JWT is accepted and refused at the permission layer afterwards. So the remaining problem is downstream of auth.

One correction to my original post: I listed broadcaster_id among the body fields. It’s a query parameter — POST /helix/extensions/chat?broadcaster_id=<id>. With it in the body the API returns 400 broadcaster_id query parameter is required. Fixed, but worth flagging since it was in the post.

On #359: we did perform a full uninstall and reinstall of the extension on the channel after 1.0 was released, not just a deactivate/reactivate. Twitch.ext.features.isChatEnabled is still false on a fresh page load afterwards, in two separate browsers. So the documented resolution didn’t resolve it here.

Current verified state:

  • Chat Capabilities is checked on the released 1.0 itself, in that version’s Extension Capabilities screen (not on a draft), and GET /helix/extensions?extension_id=<id> returns has_chat_support: true
  • GET /helix/extensions/live?extension_id=<id> lists the target channel
  • Channel is live; broadcaster_id matches the JWT’s channel_id claim
  • Client-Id and Content-Type headers set alongside Authorization
  • Extension status is Released

I tested six JWT shapes; every one that authenticates returns the byte-identical refusal:

payload result
{user_id, channel_id, role: external} not allowed
{user_id, role: external} not allowed
{channel_id, role: external} authentication failed
+ pubsub_perms: {send: [broadcast]} not allowed
{user_id, channel_id, role: broadcaster} not allowed
+ iss: <client_id> not allowed
{user_id: "U"+id, channel_id, role: broadcaster} not allowed

For a released version with Chat Capabilities enabled, has_chat_support: true, and a live activation, what else could leave the per-channel chat grant off after a full reinstall; and is there a way to inspect that grant from the API side, the way has_chat_support exposes the extension-level state? Id be happy to provide anything necessary to diagnose this issue but I’m sure Im simply missing something somewhere. Thank you for your reply and for helping with the debugging process.

The extension of clientID 3v5bihl15jwi01ord0tsrxtg49tk7h does not show as installed and active in a slot on the channel penpd TUID 108578074

I see panel 2 naty2zwfp7vecaivuve8ef1hohh6bo and panel 3 3o5i5kzs4xwq4naqstge63d4fs68l0 only

W ell that’s very odd, I definitely have it active on the channel as I just did a stream around 9pm - 12pm est and chatters were commenting on how they were “catching fish” using the extension. and when i participated in catching one it showed that another chatter was indeed in the raffle to win it. I removed the other extensions from the panels and went live again to test and see the id ‘3v5bihl15jwi01ord0tsrxtg49tk7h’ in the f12 console screen as an option from the drop down and still recieve false from the Twitch.ext.features.isChatEnabled function.
I confirmed that has_chat_support was true and that the JWT was accepted by GET /helis/extensions (with a 200) yet POST /helix/extensions/chat?broadcaster_id=108578074 still returns 401 ‘not allowed to send a chat message’.
One thing I also found strange also was that GET /users/extensions (with a user token, as documented) reports no active slots on penpd, and GET /users/extensions/list reports can_activate: false for the client, but the extensions is visibly running on stream. Is can_activate being false of note, because that to me seems like it could be the issue?

Sounds like you might have the test/not released version installed and active, and the viewers catching fish are on the testing allow list, in this case.

So if you are trying to/have installed the test/not released version, and trying to send chat to the released version thats the culprit as the versions don’t match.

Ideally you test on a test channel so you deconflict schnanigans like wrong version and/or post final testing make sure you only have the release build installed at all on your production channel.

Are you using a Channel Segment Version on the configuration service to block activation? (which I don’t think is the case here)

The final thing to do is to actually check your code so please post the code you are using to create chat messages with.

Sounds like it was installed, and something was changed on the configuration on the dev console before submitance and clearance thru the review queue. Or what is installed and active is in a funny state or what is installed and active is the test/unreleased version.

So if you have two (or more) versions of the same extension installed, we should full remove/uninstall all of them and start over. Saves misclicking and activating the wrong one.

Or per 359 you only deactivated (trashcan) instead of uninstall (dropdown uninstall) (or didn’t remove all of them to get a clean slate)

Thank you for your replay again. I’ve run some more tests and also compiled the answers to your questions:

jwtControl run with five tokens (good + four deliberately broken: wrong key, bad role, no role, expired). Result: good_token = 200, all four invalid = 401. So jwtVerdict = PROVEN; the signature, secret, HS256, role “external”, user_id/channel_id (both = broadcaster id), and exp (+30s) are all correct, and no JWT change can affect the chat 401.

Chat-send code, verbatim from server/server.js:

  • function signChatJwt(broadcasterId) {
    if (!SECRET_KEY) return null;
    const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
    const header = b64url({ alg: "HS256", typ: "JWT" });
    const payload = b64url({
    exp: Math.floor(Date.now() / 1000) + 30,
    user_id: String(broadcasterId),
    channel_id: String(broadcasterId),
    role: "external"
    });
    const sig = crypto.createHmac("sha256", SECRET_KEY).update(header + "." + payload).digest("base64url");
    return header + "." + payload + "." + sig;
    }
  • async function sendChatMessage(broadcasterId, message) {
    const token = signChatJwt(broadcasterId);
    if (!token) throw new Error("No extension secret (set TWITCH_EXTENSION_SECRET)");
    const res = await fetch(HELIX + "/extensions/chat?broadcaster_id=" + encodeURIComponent(broadcasterId), {
    method: "POST",
    headers: helixHeaders(token),
    body: JSON.stringify({
    extension_id: CLIENT_ID,
    extension_version: EXTENSION_VERSION,
    text: clampChatText(message)
    })
    });
    if (!res.ok) {
    const body = await res.text().catch(() => "");
    throw new Error("Chat send failed (" + res.status + "): " + body.slice(0, 200));
    }
    return { status: res.status, ok: true };
    }

The chat endpoint itself still returns 401 “The extension is not allowed to send a chat message to the specified broadcaster’s chat room.” on every JWT shape that reaches it, while the two shapes that don’t reach it return “authentication failed”, so auth is fine and the refusal is at the permission layer.

To your specific questions:

  • Channel Segment Version: not used. No required_configuration or channel segment in the repo; config.html has no OAuth/identity references; the uploaded zip has no manifest.json. Only the extension_version field is sent (= 1.0 = EXTENSION_VERSION on Render).

  • Installed state: only one version installed at a time, always “Aquarium Game By Penpd” with no test tag. /users/extensions/list returns one entry (1.0, overlay). Invite Only tab shows the test/deposit entries with explicit tags (1.1.1 and 1.0.1 “ASSETS UPLOADED”, 1.1 “TESTING”) while the installed entry is the released one with no tag, marked “Added”. I have uninstalled and reinstalled multiple times with no change.

  • Myself and my bot/test channel are the only ones on the access page to any of the versions, no viewer was ever added as a tester.

  • can_activate false / no slot in /users/extensions: real reading, but contradicted by the overlay working on stream (requires a slot). Recorded as activationContradiction; inconsistent data, not a root cause. The user-token slot map now agrees with the app-token one (both say no slot), which is the more reliable read, but it still disagrees with the dashboard and the working overlay.

So, JWT proven correct, capabilities enabled on the installed version, one released version installed, no channel segment version, permission toggle on, released. The refusal message says the extension is not allowed to send to that broadcaster’s chat room, and no public endpoint explains the refusal reason. At this point I’m not sure what the issue could be other than my server code being wrong somewhere or maybe the extension activation state is somehow funny like you theorized, or potentially some weird issue on twitches end (though I doubt it). If you need me to send over more of the server code or anything else, please let me know. Thanks again for your help.

Click overlay 1 → uninstall

So then you have nothing installed.

Then install it again.
And then activate it into a slot.

can_install is false as it’s already installed to your channel and calling it with your user token, so it’s checking that, and it’s not showing properly elsewhere due do GH 359 which I’m 99% sure is the case here and something was missed on tidy up/uninstall earlier

Thanks again for the reply but I systematically do that every time I change basically anything. I did it again just to be sure and still get the same issue. Is there any other reason it could show as installed for me and not for Twitch?

Steps taken:

Result:

These Twitch APIs have not changed after the reinstall:

  • GET /users/extensions/list — the endpoint that lists installed extensions per broadcaster — still returns exactly one entry for my client: 3v5bihl15jwi01ord0tsrxtg49tk7h, version 1.0, Aquarium Game By Penpd, type ["overlay"], can_activate: false. If a leftover or hidden version were really sitting on my channel, it would show here. It doesn’t. One entry before, one entry after, same version.
  • GET /helix/extensions?extension_id=...&extension_version=1.0 (signed with my extension JWT) and GET /helix/extensions/released?extension_id=...&extension_version=1.0 (app access token) both still return 1.0 as the only version they know about for this client. versionsDiscovered is still ["1.0"]. No second version appeared after reinstall.
  • GET /helix/extensions/live?extension_id=... — Twitch’s own “is this extension running on this channel” endpoint — still returns extensionActiveOnTarget: true for broadcaster 108578074. The reinstall didn’t break anything and didn’t create a different installed version.
  • extensionState: "Released" and has_chat_support: true on 1.0 are still reported by both endpoints after the reinstall. This is still the released build, still chat-capable.

Then I’m out of ideas at this point. It is none of the usual faults.

You’ve found a new variant of Required configuration segment prevents chat message and live activated channel endpoint · Issue #5 · twitchdev/issues · GitHub since you have stated that you don’t use a Required configuration segment

But I will note theres a lot of AI hallucination going around in here, I won’t list everything thats been hallucinated but theres a chunk, I just glossed over it to focus on the key points.

Edit after trying to activate it on my own channel:

You seem to have set a required version set here: as per me trying to activate it on my own channel

per this screenshot:

or this one

So you’ve run into GitHub 5 and here

you are incorrect and are using a Required Segment version.

Also there is no such thing as a manufest.json those were only used in the long dead developer rig generated by the developer rig. And I don’t know what OAuth/identity has to do here either?

You an try blanking out the configuration and/or changing the version field to something else and back and/or checking it’s correctly set to what you are expecting. You might have an incredibly dirty configuration saved at the moment.

screenshot from my Extension Tools assistance program: to example the version field

You can also try installing it to a test/development channel and retesting to see if that works as expected as the channel is “clean” instead of using yours which is “dirty” and if thats the case we need to full reset the configuration on the production/intended use channel. As I suspect this is GitHub 5 tied to a variant of GitHub 196 and the route cause of 5 is using test/release channels as the same channel on an initial release of the first public release.

I think you’re completely right and I feel bad for using up this much of your time to find it. Here is a screenshot of the configuration section on the developer console:

After reading it I realize that (i assume) I needed to put ‘1.0’ in that field instead of ‘1’, is that correct? Sorry for all the AI hallucinations as this is my first extension and I’m unfamiliar with servers and things specific to twitch (I come from a Unity developer background). I think that the state of the extension showing active for me was because it was active before release and I also tested it on my test channel so checking it there yielded the same results. If this truly is the cause I wanted to thank you for all your help, and apologize again for using up much of your time.

NO

This is a free text field, so you put whatever you want in there, and when setting configuration you provide the matching value to allow the extension to be activated. It could be 1 it could be cake it could be 1.0.

You put a 1 there so your configuration version when setting the configuration needs to be a 1

Which you are doing in order to activate the extension in a slot in the first place.

I think the issue stems from the test and release channel being the same channel.

You wouldn’t of passed review if the configuration setting to acitvate the extension was failing.

Possible solve:

Is to full reset.

You need to do a set configuration call setting the version to any other text value or blank and/or wiping the configuration.

Then uninstall

Then install, set configuration and the configuration version to 1 and activate. Then see if you can send chat.

If that doesn’t work you’ve run into GitHub 5.

Theres nothing really twitch specific to extension, they are just websites where the front end is on a different server to the backend.

Much like a game, where the game client is on a players computer and the games backend is on a server.

Only thing twitch specific is you need to include the JS helper and invoke onAuthorised, from there you don’t have to do anything else twitch specific or invoke any twitch services. It’s all optional.

To reiterate, no, the version “doesn’t have to be 1.0” it can be whatever string text value you want.

Ok that makes sense. I’ve set the version using the server side because it cant change on the developer console. Here’s how I did it:
read GET /helix/extensions/configurations?extension_id=<id> &broadcaster_id=108578074&segment=broadcaster write PUT /helix/extensions/configurations { extension_id, segment: "broadcaster", broadcaster_id: "108578074", version: <v>, content: <existing json> }

Here are the steps I went through:

  1. Read stored broadcaster version output: version “1”, content 4428 bytes

2. Write version “reset” output: 204; re-read confirms “reset”

3. Extension now shows “needs configuration” on it

4. Uninstall from the Overlay 1 dropdown; confirmed gone
from both Installed and Activated; closed and reopened the tab

5. Reinstall from Discovery

6. Write version “1” output: 204; re-read confirms “1”, content
still 4428 bytes

7. Activate into Overlay 1, hard refresh

8. Re-run the chat send

After these steps I found no change, I also attempted to use chat features by running the actual share path from the extension’s own iframe on the channel, using the token Twitch issued to it
(Twitch.ext.viewer.sessionToken) and got this as the result:

Result: 502 (bad gateway)

I also looked at the server’s health endpoint and saw these:

[share] outcome: chat_failed Chat send failed (401): {"error":"Unauthorized","status":401, "message":"The extension is not allowed to send a chat message to the specified broadcaster's chat room."}

Does this confirm it is GitHub 5? If so is there any solution? Thank you again for your reply. If there is anything else I can do/provide let me know.

Thats not how it works

The dev console you set the required version

When setting configuration on the channel you attach a version.

Then if the versions match the extension can be enabled.

So this didn’t change the version required. It changes the readiness of the channel.

We can try an alternative

  • uninstall the extension
  • reset/blank out the configuration
  • run the install

Whats a share path?

There is no posted solve for GitHub 5

Other than to not use required version which would require a new version of the extension

Thanks for your response and clarification on what I’m actually changing server side. I’ve tested on a completely new channel and also tried blanking out the config after uninstalling. Both yielded the same results which was no change in anything (401 ‘The extension is not allowed to send chat message to the specified broadcaster’s chat room.’). I believe that the extension not working on a clean channel means that the issue spans the entire extension but I’m unsure if that changes the diagnosis.

The actual share path I refer to is specific to my extension, sorry for the confusion. Sharing a fish in the game prompts it to be added to a leaderboard and shared in chat so that is what I meant when I refer to that.

I will simply develop a new version without the required version (setting ‘Broadcaster Writable Channel Segment Version’ to be blank I believe) and submit for re-review.

I want to thank you for all your help and if that is the solution then I will close the thread and mark your previous message as the solution. Thank you very much