Extension Chat Message returns 401 authentication failed

Hi, When a viewer used my extension , extension should send a message to chat about that user in that channel. Just want to send a simple message.

I’m working on “send extension chat message” also i tried “Send Extension PubSub Message” too. When i tried pubsub i was getting “403 - JWT could not be verified” error and i cant figure out it. Now i’m trying other way but i’m getting “401 authentication failed” error.
I have read a lot of explanations and posts on this subject and tried a lot of things. I am very confused now.

EBS is writen in PHP.

$payload_arr = array(
“exp” => $time,
“user_id”=> $channelid,
“role”=> “external”

$payload = json_encode($payload_arr);
// {“exp”:1592921206,“user_id”:“49354541”,“role”:“external”}

$jwt = JWT::encode($payload, $secretkey);

// I also check the token at jwt.io too.

//Send Extension Chat Message

$url = “https://api.twitch.tv/extensions/".$clientid."/".$clientversion."/channels/".$channelid."/chat”;
$headers = array(
"Authorization: Bearer ".(string)$jwt,
"Client-Id: ".$clientid,
“Content-Type: application/json”

$data = array(“text” => ‘Extension send a message to chat’);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$dresp = curl_exec($ch);
$info = curl_getinfo($ch);

Request Header:

[request_header] => POST /extensions/89sl308o9vzckjsz2viv6mx76ihask/1.1.1/channels/49354541/chat HTTP/2
Host: api.twitch.tv
accept: /
authorization: Bearer [token]
client-id: 89sl308o9vzckjsz2viv6mx76ihask
content-type: application/json
content-length: 37


{“error”:“Unauthorized”,“status”:401,“message”:“authentication failed”}

What am i doing wrong. Thanks for help.

Is the extension of the same version installed and active on the channel?
Has the streamer got the chat option enabled?



Yes it’is installed on my channel for testing and also chat options enabled.

Oh you did base64 decode your extension secret before using it to encode the JWT?

The only thing I do different is for pubsub I use, (javascript but easy enough to convert to PHP)

const sigPubSubPayload = {
    "exp": Math.floor(new Date().getTime() / 1000) + 60,
    "user_id": config.owner,
    "role": "external",
    "channel_id": "all",
    "pubsub_perms": {
        "send": [

And chat

    var sigPayload = {
        'exp':          Math.floor(new Date().getTime() / 1000) + 60,
        'user_id':      config.twitch.streamer_id,
        'role':         'broadcaster'

Since you’ve tried both PubSub and chat, and got the same issue, I bet you forgot (or didn’t know) to base decode your secret before passing it to the encoder

This is the class i used for JWT encode.
Also i copied jwt code and paste to jwt.io for check payload data. On that site its decoded like this.


so it’ll be about base64 encode/decode. Going to check that.

public static function encode($payload, $key, $alg = ‘HS256’, $keyId = null, $head = null)
$header = array(‘typ’ => ‘JWT’, ‘alg’ => $alg);

    if ($keyId !== null) {
        $header['kid'] = $keyId;

    if ( isset($head) && is_array($head) ) {
        $header = array_merge($head, $header);

    $segments = array();
    $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
    $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
    $signing_input = implode('.', $segments);
    $signature = static::sign($signing_input, $key, $alg);
    $segments[] = static::urlsafeB64Encode($signature);

    return implode('.', $segments);
public static function sign($msg, $key, $alg = 'HS256')
    if (empty(static::$supported_algs[$alg])) {
        throw new Exception('Algorithm not supported');
    list($function, $algorithm) = static::$supported_algs[$alg];
    switch($function) {
        case 'hash_hmac':
            return hash_hmac($algorithm, $msg, $key, true);
        case 'openssl':
            $signature = '';
            $success = openssl_sign($msg, $signature, $key, $algorithm);
            if (!$success) {
                throw new Exception("OpenSSL unable to sign data");
            } else {
                return $signature;

I’m using extension secret at extension client configuration.

I’ll delete key.
$base64key=“REMOVED=”; // base64 encoded key
$decodedkey = base64_decode($base64key);
printed : �����,qE�-��5�٭�2Q?"'z��7

base64 decode returns key with invalid characters. I created new extension secret but it returns invalid too.

Also I’m getting 401 error with that undecoded key too.

Removed your key as it counts as a password and shouldn’t be posted publically.

Try this instead, confirmed as working having just tested it, (it’s a bad/old/dumb test script for the chat endpoint I have kicking about in PHP)

For this $user_id should be the ID of the channel that you want to send Chat messages to.


echo date('r', time());
echo "\n";

$client_id = 'CLIENID';
$secret = base64_decode('SECRET');
$version = '0.0.1';
$user_id = 'CHANNEL_ID';

// end edits

include(__DIR__ . '/jwt.php');
$j = new JWT();

$payload = array(
    'exp' => time() + 60,
    'user_id' => ''.$user_id,
    'role' => 'broadcaster'
$bearer = $j->encode($payload, $secret);

echo "\n" . $bearer . "\n";

$data_string = json_encode(array(
    'text' => 'This is a test'

$url = 'https://api.twitch.tv/extensions/' . $client_id . '/' . $version . '/channels/' . $user_id . '/chat';

echo $url;

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_ENCODING , "gzip");
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Authorization: Bearer ' . $bearer,
    'Client-ID: ' . $client_id,
    'Content-Type: application/json'
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);

$r = curl_exec($ch);
$i = curl_getinfo($ch);


echo "\n" . $i['http_code'] . "\n";

echo "\n";
echo $r;
echo "\n";

jwt.php is from https://github.com/firebase/php-jwt/blob/master/src/JWT.php

But my current version of the file is, I only tested against my version as I know it works

Most notably is my payload differs

$payload = array(
    'exp' => time() + 60,
    'user_id' => ''.$user_id,
    'role' => 'broadcaster'


$payload = array(
    'exp' => time() + 60,
    'user_id' => '15185913',
    'role' => 'external'

Also works (my user_id and I own the extension posting to another channel)

+60 for the exp is SUPER LIBERAL really too

I checked your code and compare with mine.

I found the problem. I was encode the payload with json before create JWT.

Now it works. Thanks for help.

1 Like


Probably also worth just using the JWT lib I linked rather than custom code, but if your stuff works all the better!