Oferta de lançamento: −50 % no seu primeiro pagamento, até 30 de novembro.Ver a oferta →
webhook·toolkit
Twilio

Validação de assinatura da Twilio falhou — descubra por quê

Última atualização: julho de 2026

Você tem um cabeçalho X-Twilio-Signature, seu código diz que ele não bate, e a documentação da Twilio responde com um SDK para instalar. Isso não diz qual está errado: a URL, os parâmetros ou o token. Então cole os três abaixo: recalculamos a assinatura para cada forma plausível de URL e mostramos a que bate. A linha que bate é o seu diagnóstico.

Resumindo: uma assinatura da Twilio falha quase sempre porque a URL que o seu código reconstrói não é idêntica, byte a byte, à URL que a Twilio chamou — normalmente http:// em vez de https:// atrás de um proxy reverso. O algoritmo raramente é o problema.

Verifique uma assinatura que a Twilio rejeitou

Tudo roda no seu navegador (WebCrypto HMAC-SHA1). O seu Auth Token nunca é enviado aos nossos servidores, e nenhuma requisição sai desta página enquanto você usa a ferramenta. Abra a aba de rede e confira — preferimos que você confira.

A string exata que a Twilio assina

Todas as outras seções desta página decorrem desta. A Twilio não assina o seu corpo, nem os seus cabeçalhos. Ela assina uma única string, montada assim:

  1. Comece pela URL completa que a Twilio chamou: esquema, host, porta se ela estava explícita, caminho e query string.
  2. Ordene os nomes dos parâmetros POST em ordem ASCII.
  3. Para cada um, acrescente o nome seguido imediatamente do valor. Sem separador, sem sinal de igual, sem codificação, nada no meio.
  4. Calcule o HMAC-SHA1 da string inteira com o seu Auth Token.
  5. Codifique o digest em Base64. Isso é o X-Twilio-Signature.

O mesmo, com valores reais

URL chamada pela Twilio
https://api.example.com/twilio/sms
Parâmetros POST (form-urlencoded)
AccountSid=AC1234567890abcdef
Body=Hello
From=%2B15017122661
To=%2B15558675310
A string assinada — 101 caracteres, concatenados
https://api.example.com/twilio/smsAccountSidAC1234567890abcdefBodyHelloFrom+15017122661To+15558675310
Auth Token (descartável, só para o exemplo)
12345678901234567890123456789012
base64(HMAC-SHA1(string, token))
REsmqJpWLoqe1Q/VJXTZjx4eviY=

Repare no que não está ali. Nenhum &, nenhum =, nenhuma codificação percentual: o valor é +15017122661, não %2B15017122661. Quem reconstrói os parâmetros dividindo o corpo bruto por conta própria costuma esquecer de decodificar, e obtém uma assinatura errada por um motivo que nenhuma mensagem de erro vai mencionar.

Mude um único byte desses 101 caracteres e a assinatura muda por completo. A história inteira é essa: o seu código e a Twilio precisam montar exatamente a mesma string, e qualquer proxy, roteador ou middleware no caminho pode alterá-la sem avisar.

Quatro URLs, quatro assinaturas, uma verdade

Mesmo Auth Token, mesmos quatro parâmetros de cima. Só a URL muda — e isso basta:

URL usada para montar a stringAssinatura resultanteDe onde vem
https://api.example.com/twilio/smsREsmqJpWLoqe1Q/VJXTZjx4eviY=O que a Twilio assina
http://api.example.com/twilio/smsNpKfrAiY/l6i2mXxfWlrjyRkytY=O que o seu app vê atrás da terminação SSL
https://api.example.com:443/twilio/smsvMbUG7KsMVUK80lzK6a+4IMFPqU=Como fica uma URL montada à mão
https://api.example.com/twilio/sms/8LRHF+mPXykd0Jij9WhgsI7agMs=O que um roteador que normaliza caminhos entrega

Nada na falha diz em qual das quatro você está. É isso que a ferramenta faz: calcula todas, até dezesseis formas de URL, e compara cada uma com a assinatura que a Twilio realmente enviou. A linha verde é a resposta.

As 8 causas reais

Ordenadas pela frequência com que aparecem no fórum da comunidade Twilio e nas threads do StackOverflow sobre este erro. Essa ordem é a nossa leitura, não uma estatística medida — use-a como ordem de busca, não como fato.

#CausaO sintoma que a denunciaCorreção
1Terminação SSL (X-Forwarded-Proto)Passa com curl em localhost, falha assim que fica atrás de um load balancer. Seu log mostra http://, o Console mostra https://.Confie no proxy: app.set('trust proxy', true), ProxyFix(app.wsgi_app, x_proto=1), TrustedProxies no Laravel.
2Porta explícita na URL reconstruídaSeu log mostra https://host:443/… — a Twilio nunca escreve uma porta implícita.Monte a partir do cabeçalho Host / X-Forwarded-Host. Não concatene hostname e porta.
3Query string removida ou recodificadaSó falham as URLs com ?params. Seu framework devolveu o padrão da rota, não o alvo bruto.Use o alvo bruto da requisição (req.originalUrl, request.url) e não mexa na codificação.
4Parâmetros alterados antes da validaçãoFalha só em algumas mensagens — as que têm um campo vazio ou espaço no final.Valide antes que qualquer middleware toque no corpo. Python: parse_qs(…, keep_blank_values=True). Laravel: exclua a rota do TrimStrings.
5Corpo JSON validado como form dataWebhooks de Conversations e Studio falham, webhooks de SMS simples passam.Use validateRequestWithBody() e o parâmetro de query bodySHA256.
6Auth Token secundário não promovidoComeçou a falhar logo após uma rotação de token, em 100% das requisições.Console → Account → Auth Token: um token secundário não assina nada até você promovê-lo a Primary.
7Parâmetros repetidos (bug do SDK)Só falham mensagens em grupo e Conversations, e de forma não reproduzível.twilio-node #722: params['MessagingBinding.Address'].sort() antes de validateRequest().
8Rewrite do proxy mudando o caminhoFalha só em produção. A Twilio chama /webhooks/sms, seu app recebe /sms.Valide contra o caminho público, não o interno que sobra depois do rewrite.

O bug dos parâmetros repetidos — a culpa não é sua

Este merece uma seção própria, porque dá para ler a documentação da Twilio por uma hora sem encontrá-lo. Mensagens em grupo e Conversations enviam a mesma chave várias vezes:

O corpo que a Twilio envia via POST
MessagingBinding.Address=%2B15558675310&MessagingBinding.Address=%2B15017122661

O servidor da Twilio ordena as chaves e depois concatena. O seu mapa de parâmetros contém um array. O twilio-node faz data += key + params[key], e o JavaScript converte esse array em string juntando os itens com vírgulas — na ordem de chegada. Se o seu framework leu os dois endereços em uma ordem diferente da usada pelo servidor da Twilio, as duas strings diferem e a validação falha. Mesmo token, mesma URL, mesmos parâmetros. Falha mesmo assim.

Duas ordens, duas assinaturas — todo o resto idêntico
Ordem de chegada: +15558675310 e depois +15017122661LJaNX3wj9/RRc4+GG1kwgPndM1c=
Ordenado: +15017122661 e depois +15558675310A7jNj66u8x1pDaqy3CNma9Q0XvA=
A solução de contorno
// twilio-node issue #722
// Parâmetros repetidos chegam como array; ordene antes de validar.
if (Array.isArray(params['MessagingBinding.Address'])) {
  params['MessagingBinding.Address'].sort();
}
const valid = twilio.validateRequest(authToken, signature, url, params);

A ferramenta testa as duas ordens automaticamente. Se a linha ordenada ficar verde, você encontrou um bug no SDK, não no seu código — e pode parar de reler o seu handler.

Funciona localmente, quebra em produção

A história mais comum de todas, e ela tem uma única causa: em dev, o seu túnel entrega a requisição ao app no mesmo esquema que a Twilio usou. Em produção, Traefik, nginx ou Cloudflare terminam o TLS e repassam HTTP simples na porta 3000. O seu framework informa o que vê no próprio socket — http:// — e assina uma string que a Twilio nunca montou.

Express
// Atrás de Traefik / nginx / Cloudflare
app.set('trust proxy', true);
// req.protocol agora segue o X-Forwarded-Proto
const url = req.protocol + '://' + req.get('host') + req.originalUrl;
Flask / Werkzeug
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
# request.url agora reconstrói https://
validator.validate(request.url, request.form, signature)
Laravel
// app/Http/Middleware/TrustProxies.php
protected $proxies = '*';           // só se o proxy for seu
protected $headers = Request::HEADER_X_FORWARDED_ALL;

Uma linha, três ecossistemas. Com o proxy marcado como confiável, o framework lê X-Forwarded-Proto e X-Forwarded-Host, reconstrói https://, e a assinatura bate. Só confie em proxies que você realmente controla: com trust proxy em true, qualquer coisa à frente pode declarar o esquema que quiser.

Corpos JSON: bodySHA256

Os webhooks da Twilio são application/x-www-form-urlencoded por padrão — nunca JSON. Mas Conversations e Studio podem enviar JSON, e aí as regras mudam: o corpo simplesmente não está nos parâmetros. A Twilio calcula o hash dele com SHA-256, acrescenta esse hash à URL como parâmetro de query bodySHA256 e assina a URL com uma lista de parâmetros vazia.

Validando um webhook JSON (Node)
const valid = twilio.validateRequestWithBody(
  process.env.TWILIO_AUTH_TOKEN,
  req.headers['x-twilio-signature'],
  fullUrl,        // ainda precisa ter ?bodySHA256=…
  rawBody         // a string bruta, NÃO um JSON.parse seguido de stringify
);

Duas armadilhas aqui. Remova a query string da URL e você remove também o bodySHA256, então a verificação falha sem nunca mencionar o corpo. E se o seu framework parseou o JSON antes de você, JSON.stringify() não vai devolver os bytes originais — a ordem das chaves e os espaços se perderam. Capture o corpo bruto. O modo JSON da ferramenta acima mostra os dois hashes lado a lado para você ver qual das duas armadilhas te pegou.

Fatos sobre a Twilio que vale a pena ter em mente

Detalhes que decidem se a assinatura chega a ter a chance de ser verificada:

  • HMAC-SHA1, não SHA256. Stripe, GitHub e Shopify usam SHA-256, então copiar um handler que funciona de um deles e trocar o nome do cabeçalho produz um validador que nunca bate com nada.
  • O payload é application/x-www-form-urlencoded por padrão, nunca JSON. Se o seu framework só parseia corpos JSON, o seu mapa de parâmetros fica vazio e toda assinatura falha.
  • A Twilio espera 15 segundos por um webhook de voz, 5 segundos no Conversations. Valide primeiro, responda, e depois faça o trabalho lento — uma verificação de assinatura que roda depois de uma chamada ao banco pode estourar o tempo antes de retornar.
  • Uma nova tentativa por padrão, e até cinco com connection overrides. Então uma assinatura ruim não é uma mensagem com falha, é uma mensagem perdida depois de algumas tentativas.
  • O Debugger guarda os logs por 30 dias. É lá que você encontra a URL que a Twilio realmente chamou, que é a entrada de que esta página precisa.
  • Em conexões WebSocket o cabeçalho chega em minúsculas: x-twilio-signature. Compare cabeçalhos sem diferenciar maiúsculas e minúsculas.

E o erro 57012 da Twilio?

O erro 57012 (assinatura inválida) diz que a assinatura não bate com a que a Twilio calculou, e aponta os suspeitos de sempre: token errado, URL errada, parâmetros alterados no caminho, hash do corpo ausente. Tudo verdade. Mas ele para exatamente onde a depuração começa, porque não consegue dizer qual deles se aplica à sua requisição. Note também que, do lado da Twilio, as causas 1, 2, 3, 7 e 8 acima são todas apenas “URL errada” — cinco bugs diferentes, cinco correções diferentes, um só código de erro. Separá-los é o objetivo desta página.

Quando usar o quê — com honestidade

Não somos a resposta para tudo aqui, e fingir o contrário desperdiçaria a sua tarde.

A documentação da Twilio

Você está escrevendo o handler do zero e quer o algoritmo de referência e o SDK da sua linguagem. É a fonte da verdade — webhooks-security cobre sete linguagens. Ela não tem nenhuma seção de troubleshooting, e é por isso que você veio parar aqui.

Hookdeck

O seu problema não é uma requisição quebrada, é a entrega: você quer retries sob o seu controle, uma dead letter queue, timeouts além dos 15 segundos da Twilio, transformação de form para JSON, ou verificação de assinatura na borda para que o seu app nunca veja uma requisição não verificada. Isso é uma plataforma, e das boas. Pague por ela.

Esta página

Uma assinatura, agora, e você quer saber qual linha do seu código mente. Trinta segundos, nada para instalar, sem conta. Depois você vai corrigir a config do proxy e esquece que a gente existe. É um ótimo desfecho.

Se webhooks são parte permanente da sua stack, e não um incêndio que você está apagando, o nosso túnel Relay encaminha o tráfego real da Twilio para localhost, e o testador de webhooks da Twilio reenvia requisições assinadas ao seu handler para você conferir a correção sem gastar um SMS de verdade.

Testador de webhook TwilioRelay

Perguntas frequentes

Por que a minha assinatura da Twilio falha atrás do ngrok ou de um proxy?

Porque o proxy termina o TLS e repassa HTTP simples ao seu app. A Twilio assinou a URL https://; o seu framework reconstrói http:// a partir do próprio socket. As strings diferem, o HMAC difere. Marque o proxy como confiável para que o X-Forwarded-Proto seja usado na reconstrução da URL, e ela volta a bater.

A porta deve entrar na URL?

Só se a Twilio a usou na chamada. A Twilio assina a URL como foi configurada, então um 443 padrão nunca aparece na string. Acrescente :443 por conta própria ao reconstruir e você obtém outra assinatura. Reconstrua o host a partir do cabeçalho Host em vez de concatenar hostname e porta.

Como validar um corpo JSON vindo da Twilio?

Com validateRequestWithBody() em vez de validateRequest(). A Twilio calcula o hash do corpo com SHA-256, coloca esse hash em um parâmetro de query bodySHA256 e assina a URL sem parâmetros. Você precisa dos bytes brutos do corpo e da URL completa, incluindo esse parâmetro de query.

Por que funciona localmente mas não em produção?

Em dev, o seu túnel normalmente entrega ao app o mesmo esquema que a Twilio usou. Em produção, Traefik, nginx ou Cloudflare entregam HTTP simples em uma porta interna. Mesmo código, URL reconstruída diferente, então a assinatura deixa de bater. É a causa número um desta página.

O meu Auth Token é enviado para os servidores de vocês?

Não. O HMAC roda no seu navegador com WebCrypto, e a ferramenta não envia nenhuma requisição enquanto você a usa. Abra a aba de rede e confira. O seu Auth Token é um segredo de produção — você deveria fazer essa pergunta a todo site que o pede, inclusive o nosso.

A assinatura da Twilio é SHA-256, como a da Stripe?

Não, a Twilio usa HMAC-SHA1 e codifica em base64. Stripe, GitHub e Shopify usam SHA-256. Adaptar um handler da Stripe que funciona só renomeando o cabeçalho gera um validador que rejeita tudo, e é um caminho comum para chegar a este erro.

Oferta de lançamento · −50 %

Depurando uma integração de webhook esta semana?

O Pass 7 dias desbloqueia as explicações com IA e o código do handler pronto para colar, 25 URLs permanentes, 30 dias de histórico e o túnel Relay para localhost. Um único pagamento, sem assinatura.

€ 5 € 2,50 uma vez
Pagamento único · sem assinatura · reembolso em 14 dias · preço de lançamento até 30 de novembro
Validação de assinatura da Twilio falhou: encontre a causa · Webhook Toolkit