Twilio Error 31201: Fix the Unknown Authorization Error
Twilio error 31201 is an unknown authorization error: Twilio rejected your Access Token without saying why. How to decode it and fix the seven causes.
Twilio error 31201 is an unknown authorization error: Twilio rejected your Access Token without saying why. How to decode it and fix the seven causes.
Twilio error 31201 ("Unknown authorization error") means Twilio's signaling gateway rejected your Access Token and could not describe the rejection with a more specific code. The 312xx range has a dedicated code for nearly every token failure — bad signature, expired, malformed, dead account — so landing on 31201 usually means the rejection happened before those checks ran, because Twilio could not resolve the credentials your token points at. This guide shows you how to decode the token, verify it claim by claim, and fix the seven configurations that produce a well-formed token Twilio still refuses.
31201 opens Twilio's 312xx authorization range for the Voice SDK, where the error codes reference defines it as "Unknown authorization error." Every other code in that range names its failure: 31202 is a signature that failed validation, 31203 an invalid or inactive account, 31204 a malformed JWT, 31205 an expired one, 31207 a TTL longer than Twilio permits. Each of those fires because Twilio got far enough to run that specific check and the check failed.
31201 is the residual bucket. The token was rejected, and none of the named checks is what rejected it — which points at the credentials the token references rather than the token's own structure. An API Key SID Twilio cannot find, a key in the wrong account, the wrong region, or the wrong key class all land here, because Twilio never gets a secret to validate the signature against and therefore never reaches 31202.
That vagueness is why the error has stayed mysterious for a decade. The Stack Overflow thread that still ranks for this code, "Receiving Twilio 31201 error, despite having well-formed token", was posted in June 2016 and has zero answers.
Two unrelated Twilio errors carry the number 31201, and mixing them up sends you debugging the wrong layer for an afternoon.
Device registration or at connect time. That is the one this post covers.twilio.js 1.x) when the browser has already granted permission but getUserMedia cannot acquire an input stream.Version tells them apart. Twilio renumbered the microphone failure to 31402 in Voice JS SDK 2.x, so on @twilio/voice-sdk v2 a 31201 is always authorization. On the older 1.x Twilio.Device, check the timing: an error at device.setup() or registration is the auth code, while one thrown at connect() or accept() is the microphone code — and its fix is a media fix (a stale deviceId passed to setInputDevice, over-tight audio constraints, or a headset another app is holding).
Take the exact string your server handed the browser, from the network response rather than from your source file. Half the 31201 reports we have seen come from a server that is generating a correct token and a client that is sending a different one — a cached copy, a stale env var, an old build.
Paste it into jwt.io and read the decoded payload, or dump it locally without sending your token to a website:
node -e "const p=process.argv[1].split('.');for(const s of p.slice(0,2))console.log(JSON.stringify(JSON.parse(Buffer.from(s,'base64url')),null,2))" "$TOKEN"
That prints the header and the payload. Check them against this list:
| Field | Must be | Frequent wrong value |
|---|---|---|
alg (header) | HS256 | HS512, or none from a hand-rolled signer |
cty (header) | twilio-fpa;v=1 | absent, when the JWT was built by a generic library |
iss | Your API Key SID, starting SK | An AC Account SID |
sub | The Account SID that owns the key, starting AC | The parent account when the key is on a subaccount |
exp | A Unix timestamp in the future | Milliseconds instead of seconds |
grants.identity | Non-empty, alphanumeric and underscores only | An email address |
grants.voice | Present, with outgoing.application_sid | Missing entirely |
An iss that starts with AC is the single highest-yield thing to look for. It means the token was signed as though your Account SID were an API Key, and Twilio has nothing to look up.
If exp is in the past, stop here — that is 31205, and the fix is a refresh strategy rather than a credentials audit. We cover it in the guide to Twilio JWT token expiry.
Every cause below produces a JWT that decodes cleanly and looks right in jwt.io. That is what makes 31201 frustrating: the token passes inspection and Twilio still refuses it.
The AccessToken constructor takes (accountSid, apiKeySid, apiKeySecret, options). The third argument is the 32-character secret Twilio shows exactly once, when the key is created — never your account's Auth Token.
Substituting the Auth Token while keeping a real SK SID in iss normally produces 31202, since Twilio finds the key and the signature fails. The 31201 version is the sloppier variant: teams pass the Account SID as the second argument too, so iss becomes an AC SID. Now there is no key to resolve, no secret to check, and no specific failure to report.
Deleting an API Key in the Console invalidates every token signed with it immediately. Rotating a secret without redeploying the server that holds it has the same effect. The usual shape of this bug is an environment variable that was set once, during a spike, pointing at a key somebody cleaned up months later.
Take the SK SID from your decoded iss and search for it in Console → Account → API keys & tokens. If it is not listed there, you have found the problem, and no amount of token-generation debugging will help.
Twilio has three key classes: Main (full access to all API resources), Standard (everything except the Accounts and Keys resources), and Restricted (fine-grained, per-resource permissions).
Main and Standard both mint Access Tokens. Restricted keys do not — Twilio's documentation for error 31204 states it directly: "Use Main or Standard API Keys to create Access Tokens, as Restricted API Keys aren't supported for Access Token generation" (as of August 2026). Security reviews walk into this one. A team replaces a working Standard key with a Restricted key scoped tightly to Voice, deploys, and every browser client stops authorizing. No Voice permission you can add to a Restricted key changes the outcome.
sub account belong to different accountsAPI Keys live inside one account. If you run subaccounts — a common pattern for per-tenant isolation — the key you sign with and the Account SID in sub have to be the same account. A parent-account key with a subaccount sub (or the reverse) is an unresolvable pairing, and Twilio reports the generic authorization failure rather than naming the mismatch.
Twilio's Access Tokens documentation is narrow here: "Voice tokens may only contain alpha-numeric and underscore characters." Email addresses are the standard trap, since [email protected] contains three forbidden characters and is the most natural user identifier to reach for. Hyphenated UUIDs fail too.
An empty identity fails for a different reason: Twilio's 31301 registration error docs list "The Access Token does not include a non-empty identity" as a cause. Hash or transliterate — alice_example_com, or an opaque internal user ID — and keep the mapping on your server.
Building the grant and forgetting token.addGrant(voiceGrant) is a one-line omission that produces a perfectly valid JWT with no voice permissions in it. Twilio names this cause explicitly in the 31301 docs: "The Access Token does not include a VoiceGrant, or the API key and secret used to generate the token are incorrect."
An outgoingApplicationSid pointing at a TwiML App that was deleted, or that lives in a different account than sub, has the same effect — the grant exists but references nothing resolvable. Confirm the AP SID in Console → Voice → TwiML Apps under the same account as your key.
Access Tokens carry a target region, defaulting to us1 when you do not set one. Twilio's guidance for non-US regions is unambiguous: "The Twilio resources referred to by the Access Token (the API Key, TwiML Application, and Push Credential) must exist in the Twilio Region specified in the Access Token."
Credentials are themselves region-scoped — an API Key created while the Console's region selector was set to IE1 does not exist in US1 at all. An Irish key inside a default-region token therefore hands Twilio an SK SID it genuinely cannot find, which is 31201 by the same mechanism as a deleted key. Pass region: 'ie1' in the AccessToken options (the region parameter is currently Voice-only) and create the API Key and TwiML App in that region. Setting edge on the client Device changes only the network entry point and will not fix a region mismatch in the token.
If the SDK reported something other than 31201, fix that code instead — the specific codes are far cheaper to diagnose.
| Code | What it means | Where the fix lives |
|---|---|---|
| 31201 | Unknown authorization error | Credentials the token references — this page |
| 31202 | JWT signature validation failed | Wrong API Key secret, or alg other than HS256 |
| 31203 | Invalid or inactive account | Account suspended, closed, or unfunded — check the Console |
| 31204 | Invalid JWT token | Malformed JWT, or missing identity/VoiceGrant |
| 31205 | JWT token has expired | Refresh on the SDK's tokenWillExpire event |
| 31207 | Expiration interval is too long | ttl above Twilio's 24-hour maximum |
Two neighbours worth knowing: a bare error 31000 can hide an authorization failure underneath, so check twilioError.originalError before assuming it is unrelated. And once the token authorizes but the device still will not come up, you have left token territory for registration — that is covered in Twilio device offline.
Here is the server-side shape that satisfies all seven causes above, in Node:
const AccessToken = require('twilio').jwt.AccessToken;
const VoiceGrant = AccessToken.VoiceGrant;
const voiceGrant = new VoiceGrant({
outgoingApplicationSid: process.env.TWILIO_TWIML_APP_SID, // AP..., same account as sub
incomingAllow: true,
});
const token = new AccessToken(
process.env.TWILIO_ACCOUNT_SID, // AC... → sub
process.env.TWILIO_API_KEY_SID, // SK... → iss (Main or Standard key, never Restricted)
process.env.TWILIO_API_KEY_SECRET,// the 32-char secret, not the Auth Token
{
identity: userId.replace(/[^a-zA-Z0-9_]/g, '_'), // alphanumeric + underscore only
ttl: 3600, // 24h max; short is safer
}
);
token.addGrant(voiceGrant); // the line people forget
res.json({ token: token.toJwt() });
Log the decoded payload of a freshly minted token once in staging and eyeball it against the claim table above. It takes ten seconds and rules out five of the seven causes.
Where do I find my Twilio API Key SID and Secret? In the Console under Account → API keys & tokens, where you can create a Standard or Main key. The secret is displayed only at creation time and cannot be retrieved afterwards — if you have lost it, create a new key and update your server, since rotating invalidates existing tokens instantly.
Does error 31201 happen on iOS and Android too?
Yes. The token path is identical across the JavaScript, iOS, and Android Voice SDKs, and the long-running twilio/voice-quickstart-android issue #166 tracks exactly this. On mobile it typically surfaces during push registration rather than at connect time, which makes the missing-VoiceGrant and empty-identity causes the ones to check first.
Is 31201 caused by a Twilio outage or a suspended account? Neither, usually. A suspended, closed, or unfunded account produces 31203 specifically, not 31201, and platform incidents show up at status.twilio.com, which rules that out in seconds. A fresh 31201 on a deploy that changed no Twilio configuration is almost always a changed environment variable on your own server.
How long should a Twilio Voice access token last?
Twilio caps token age at 24 hours and the helper libraries default to one hour. Ask for the shortest TTL your session length tolerates, then refresh in the browser on the tokenWillExpire event rather than lengthening it. A 24-hour token that leaks is a 24-hour problem.
What is the difference between 31201 and 31002? Sequence. 31201 means the token was never accepted, so no call was attempted. 31002 Connection Declined means authorization succeeded and Twilio then refused the call itself — trial-account restrictions, geo permissions, or a TwiML App problem. Different layers, different fixes.
Every cause above is a decision your token endpoint has to get right and then keep right forever: which key class, which account, which region, which grant, which identity characters. Alloqui takes those decisions off your desk. You connect your own Twilio account, Alloqui creates the API Key and constructs the VoiceGrant server-side from your credentials, and the <Dialer /> component receives a token that is correct by construction. Wrong key type and malformed grant stop being failure modes you can reach.