Oferta startowa: −50 % na pierwszą płatność, do 30 listopada.Zobacz ofertę →
webhook·toolkit
Twilio

Walidacja podpisu Twilio nie powiodła się — sprawdź dlaczego

Ostatnia aktualizacja: lipiec 2026

Masz nagłówek X-Twilio-Signature, Twój kod twierdzi, że się nie zgadza, a dokumentacja Twilio odpowiada SDK do zainstalowania. To nie mówi Ci, co jest źle: URL, parametry czy token. Wklej więc wszystkie trzy poniżej: przeliczamy podpis dla każdej prawdopodobnej postaci URL i pokazujemy tę, która pasuje. Pasujący wiersz to Twoja diagnoza.

W skrócie: podpis Twilio prawie zawsze zawodzi, bo URL odtworzony przez Twój kod nie jest bajt w bajt identyczny z URL-em, który wywołało Twilio — najczęściej http:// zamiast https:// za reverse proxy. Algorytm rzadko jest problemem.

Sprawdź podpis odrzucony przez Twilio

Wszystko działa w Twojej przeglądarce (WebCrypto HMAC-SHA1). Twój Auth Token nigdy nie jest wysyłany na nasze serwery, a podczas korzystania z narzędzia żadne żądanie nie opuszcza tej strony. Otwórz zakładkę sieci i sprawdź — wręcz do tego zachęcamy.

Dokładny ciąg, który podpisuje Twilio

Wszystko inne na tej stronie wynika z tej sekcji. Twilio nie podpisuje body ani nagłówków. Podpisuje jeden ciąg, zbudowany tak:

  1. Zacznij od pełnego URL-a, który wywołało Twilio: schemat, host, port (jeśli był podany jawnie), ścieżka i query string.
  2. Posortuj nazwy parametrów POST w kolejności ASCII.
  3. Dla każdego dopisz nazwę, a zaraz po niej wartość. Bez separatora, bez znaku równości, bez kodowania, nic pomiędzy.
  4. Policz HMAC-SHA1 z całego ciągu, używając swojego Auth Tokena.
  5. Zakoduj wynik w Base64. To jest X-Twilio-Signature.

To samo na prawdziwych wartościach

URL wywołany przez Twilio
https://api.example.com/twilio/sms
Parametry POST (form-urlencoded)
AccountSid=AC1234567890abcdef
Body=Hello
From=%2B15017122661
To=%2B15558675310
Podpisywany ciąg — 101 znaków, sklejonych
https://api.example.com/twilio/smsAccountSidAC1234567890abcdefBodyHelloFrom+15017122661To+15558675310
Auth Token (jednorazowy, na potrzeby przykładu)
12345678901234567890123456789012
base64(HMAC-SHA1(ciąg, token))
REsmqJpWLoqe1Q/VJXTZjx4eviY=

Zobacz, czego tu nie ma. Żadnego &, żadnego =, żadnego percent-encodingu: wartość to +15017122661, a nie %2B15017122661. Kto sam odtwarza parametry, dzieląc surowe body, zwykle zapomina je zdekodować — i dostaje błędny podpis z powodu, o którym żaden komunikat błędu nigdy nie wspomni.

Zmień jeden bajt z tych 101 znaków, a podpis zmieni się całkowicie. To cała historia: Twój kod i Twilio muszą zbudować dokładnie ten sam ciąg, a każde proxy, router i middleware po drodze może go zmienić bez słowa.

Cztery URL-e, cztery podpisy, jedna prawda

Ten sam Auth Token, te same cztery parametry co wyżej. Zmienia się tylko URL — i to wystarczy:

URL użyty do zbudowania ciąguOtrzymany podpisSkąd się bierze
https://api.example.com/twilio/smsREsmqJpWLoqe1Q/VJXTZjx4eviY=To, co podpisuje Twilio
http://api.example.com/twilio/smsNpKfrAiY/l6i2mXxfWlrjyRkytY=To, co widzi Twoja aplikacja za terminacją SSL
https://api.example.com:443/twilio/smsvMbUG7KsMVUK80lzK6a+4IMFPqU=Tak wygląda URL sklejany ręcznie
https://api.example.com/twilio/sms/8LRHF+mPXykd0Jij9WhgsI7agMs=To daje router normalizujący ścieżki

Nic w samym błędzie nie mówi, w którym z czterech przypadków jesteś. Od tego jest narzędzie: liczy wszystkie, do szesnastu postaci URL, i porównuje każdą z podpisem, który Twilio faktycznie wysłało. Zielony wiersz to odpowiedź.

8 prawdziwych przyczyn

Uporządkowane według tego, jak często pojawiają się na forum społeczności Twilio i w wątkach StackOverflow o tym błędzie. Ta kolejność to nasza interpretacja, a nie zmierzona statystyka — traktuj ją jako kolejność szukania, nie jako fakt.

#PrzyczynaObjaw, który ją zdradzaRozwiązanie
1Terminacja SSL (X-Forwarded-Proto)Działa z curl na localhost, przestaje działać, gdy tylko trafi za load balancer. Twój log pokazuje http://, a konsola https://.Zaufaj proxy: app.set('trust proxy', true), ProxyFix(app.wsgi_app, x_proto=1), TrustedProxies w Laravelu.
2Jawny port w odtworzonym URL-uTwój log pokazuje https://host:443/… — Twilio nigdy nie wpisuje portu domyślnego.Buduj z nagłówka Host / X-Forwarded-Host. Nie sklejaj hostname i portu.
3Query string pominięty lub przekodowanyZawodzą tylko URL-e z ?parametrami. Framework zwrócił wzorzec trasy, a nie surowy target.Użyj surowego targetu żądania (req.originalUrl, request.url) i nie ruszaj kodowania.
4Parametry zmienione przed walidacjąZawodzi tylko przy niektórych wiadomościach — tych z pustym polem lub spacją na końcu.Waliduj, zanim jakikolwiek middleware dotknie body. Python: parse_qs(…, keep_blank_values=True). Laravel: wyłącz trasę z TrimStrings.
5Body JSON walidowane jak dane formularzaWebhooki Conversations i Studio zawodzą, zwykłe webhooki SMS przechodzą.Użyj validateRequestWithBody() i parametru query bodySHA256.
6Niewypromowany drugi Auth TokenZaczęło zawodzić tuż po rotacji tokena, na 100% żądań.Console → Account → Auth Token: token pomocniczy niczego nie podpisuje, dopóki nie wypromujesz go na Primary.
7Powtórzone parametry (błąd SDK)Zawodzą tylko wiadomości grupowe i Conversations, i to nie w sposób powtarzalny.twilio-node #722: params['MessagingBinding.Address'].sort() przed validateRequest().
8Rewrite w proxy zmieniający ścieżkęZawodzi tylko na produkcji. Twilio wywołuje /webhooks/sms, Twoja aplikacja dostaje /sms.Waliduj względem publicznej ścieżki, a nie wewnętrznej, która zostaje po rewrite.

Błąd powtórzonych parametrów — to nie Twoja wina

Ta przyczyna zasługuje na osobną sekcję, bo możesz czytać dokumentację Twilio godzinę i nigdy jej nie znaleźć. Wiadomości grupowe i Conversations wysyłają ten sam klucz kilka razy:

Body, które Twilio wysyła POST-em
MessagingBinding.Address=%2B15558675310&MessagingBinding.Address=%2B15017122661

Serwer Twilio sortuje klucze, a potem skleja. Twoja mapa parametrów zawiera tablicę. twilio-node robi data += key + params[key], a JavaScript zamienia tę tablicę na tekst, łącząc elementy przecinkami — w kolejności nadejścia. Jeśli Twój framework sparsował oba adresy w innej kolejności niż serwer Twilio, ciągi się różnią i walidacja zawodzi. Ten sam token, ten sam URL, te same parametry. A i tak zawodzi.

Dwie kolejności, dwa podpisy — cała reszta identyczna
Kolejność nadejścia: +15558675310, potem +15017122661LJaNX3wj9/RRc4+GG1kwgPndM1c=
Posortowane: +15017122661, potem +15558675310A7jNj66u8x1pDaqy3CNma9Q0XvA=
Obejście
// twilio-node issue #722
// Powtórzone parametry przychodzą jako tablica; posortuj je przed walidacją.
if (Array.isArray(params['MessagingBinding.Address'])) {
  params['MessagingBinding.Address'].sort();
}
const valid = twilio.validateRequest(authToken, signature, url, params);

Narzędzie automatycznie testuje obie kolejności. Jeśli posortowany wiersz świeci na zielono, błąd tkwi w SDK, a nie w Twoim kodzie — i możesz przestać po raz kolejny czytać swój handler.

Lokalnie działa, na produkcji się sypie

Najczęstsza historia i ma jedną przyczynę: w dev Twój tunel przekazuje żądanie do aplikacji tym samym schematem, którego użyło Twilio. Na produkcji Traefik, nginx lub Cloudflare terminują TLS i przekazują zwykłe HTTP na port 3000. Twój framework raportuje to, co widzi na swoim sockecie — http:// — i podpisuje ciąg, którego Twilio nigdy nie zbudowało.

Express
// Za Traefikiem / nginx / Cloudflare
app.set('trust proxy', true);
// req.protocol teraz podąża za 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 teraz odtwarza https://
validator.validate(request.url, request.form, signature)
Laravel
// app/Http/Middleware/TrustProxies.php
protected $proxies = '*';           // tylko jeśli proxy jest Twoje
protected $headers = Request::HEADER_X_FORWARDED_ALL;

Jedna linijka, trzy ekosystemy. Gdy proxy jest zaufane, framework czyta X-Forwarded-Proto i X-Forwarded-Host, odtwarza https:// i podpis się zgadza. Ufaj tylko proxy, które faktycznie kontrolujesz: przy trust proxy ustawionym na true wszystko, co stoi przed aplikacją, może podać dowolny schemat.

Body JSON: bodySHA256

Webhooki Twilio są domyślnie application/x-www-form-urlencoded — nigdy JSON. Ale Conversations i Studio potrafią wysyłać JSON, a wtedy zasady się zmieniają: body w ogóle nie trafia do parametrów. Twilio hashuje je SHA-256, dopisuje ten hash do URL-a jako parametr query bodySHA256 i podpisuje URL z pustą listą parametrów.

Walidacja webhooka JSON (Node)
const valid = twilio.validateRequestWithBody(
  process.env.TWILIO_AUTH_TOKEN,
  req.headers['x-twilio-signature'],
  fullUrl,        // musi nadal zawierać ?bodySHA256=…
  rawBody         // surowy ciąg, NIE po JSON.parse i ponownym stringify
);

Są tu dwie pułapki. Usuń query string z URL-a, a usuniesz też bodySHA256 — weryfikacja zawiedzie, nie wspominając nawet o body. A jeśli framework sparsował JSON, zanim trafił on do Twojego kodu, JSON.stringify() nie odda Ci oryginalnych bajtów — kolejność kluczy i białe znaki przepadły. Przechwyć surowe body. Tryb JSON w narzędziu powyżej pokazuje oba hashe obok siebie, więc od razu widać, która z dwóch pułapek zadziałała.

Fakty o Twilio, które warto mieć w głowie

Szczegóły, od których zależy, czy podpis w ogóle ma szansę zostać sprawdzony:

  • HMAC-SHA1, a nie SHA256. Stripe, GitHub i Shopify używają SHA-256, więc skopiowanie działającego handlera od któregoś z nich i podmiana nazwy nagłówka daje walidator, który nigdy niczego nie dopasuje.
  • Payload jest domyślnie application/x-www-form-urlencoded, nigdy JSON. Jeśli Twój framework parsuje tylko body JSON, mapa parametrów jest pusta i każdy podpis zawodzi.
  • Twilio czeka 15 sekund na webhook głosowy i 5 sekund w Conversations. Najpierw waliduj, potem odpowiedz, a dopiero potem wykonuj powolną pracę — sprawdzenie podpisu uruchomione po zapytaniu do bazy może przekroczyć limit czasu, zanim zwróci wynik.
  • Domyślnie jedna ponowna próba, a z nadpisaniem ustawień połączenia do pięciu. Zły podpis to więc nie jedna nieudana wiadomość, tylko wiadomość utracona po kilku próbach.
  • Debugger przechowuje logi przez 30 dni. Tam znajdziesz URL, który Twilio faktycznie wywołało — czyli dane wejściowe, których potrzebuje ta strona.
  • Przy połączeniach WebSocket nagłówek przychodzi małymi literami: x-twilio-signature. Porównuj nagłówki bez rozróżniania wielkości liter.

A błąd Twilio 57012?

Błąd 57012 (nieprawidłowy podpis) mówi, że podpis nie zgadza się z tym, który policzyło Twilio, i wskazuje zwykłych podejrzanych: zły token, zły URL, parametry zmienione po drodze, brak hasha body. Wszystko prawda. Ale kończy się dokładnie tam, gdzie zaczyna się debugowanie, bo nie potrafi powiedzieć, który z nich dotyczy Twojego żądania. Zauważ też, że z perspektywy Twilio przyczyny 1, 2, 3, 7 i 8 powyżej to po prostu „zły URL” — pięć różnych błędów, pięć różnych poprawek, jeden kod błędu. Rozdzielenie ich to cały sens tej strony.

Czego kiedy używać — uczciwie

Nie jesteśmy tu odpowiedzią na wszystko, a udawanie, że jest inaczej, zmarnowałoby Ci popołudnie.

Dokumentacja Twilio

Piszesz handler od zera i potrzebujesz referencyjnego algorytmu oraz SDK dla swojego języka. To źródło prawdy — webhooks-security obejmuje siedem języków. Nie ma za to żadnej sekcji o rozwiązywaniu problemów, i dlatego jesteś tutaj.

Hookdeck

Twój problem to nie jedno zepsute żądanie, tylko dostarczanie: chcesz ponownych prób pod swoją kontrolą, dead letter queue, timeoutów dłuższych niż 15 sekund Twilio, konwersji formularza na JSON albo weryfikacji podpisu na brzegu, żeby aplikacja nigdy nie widziała niezweryfikowanego żądania. To platforma, i to prawdziwa. Zapłać za nią.

Ta strona

Jeden podpis, teraz, i chcesz wiedzieć, która linijka Twojego kodu kłamie. Trzydzieści sekund, nic do instalowania, bez konta. Potem idziesz poprawić konfigurację proxy i zapominasz, że istniejemy. To dobre zakończenie.

Jeśli webhooki są stałym elementem Twojego stacku, a nie pożarem do ugaszenia, nasz tunel Relay przekazuje prawdziwy ruch Twilio do localhost, a tester webhooków Twilio odtwarza podpisane żądania na Twoim handlerze, więc sprawdzisz poprawkę bez wysyłania prawdziwego SMS-a.

Tester webhooków TwilioRelay

Najczęstsze pytania

Dlaczego mój podpis Twilio nie przechodzi za ngrokiem lub proxy?

Bo proxy terminuje TLS i przekazuje do aplikacji zwykłe HTTP. Twilio podpisało URL https://, a Twój framework odtwarza http:// ze swojego socketu. Ciągi się różnią, więc HMAC też. Zaufaj proxy, żeby URL był odtwarzany z X-Forwarded-Proto, i podpis znów się zgodzi.

Czy port powinien być w URL-u?

Tylko jeśli Twilio go wywołało. Twilio podpisuje URL w takiej postaci, w jakiej jest skonfigurowany, więc standardowy 443 nigdy nie pojawia się w ciągu. Dopisz :443 sam przy odtwarzaniu, a dostaniesz inny podpis. Odtwarzaj host z nagłówka Host zamiast sklejać hostname i port.

Jak zwalidować body JSON od Twilio?

Za pomocą validateRequestWithBody() zamiast validateRequest(). Twilio hashuje body SHA-256, umieszcza hash w parametrze query bodySHA256 i podpisuje URL bez parametrów. Potrzebujesz surowych bajtów body i pełnego URL-a razem z tym parametrem query.

Dlaczego działa lokalnie, a na produkcji nie?

W dev Twój tunel zwykle przekazuje aplikacji ten sam schemat, którego użyło Twilio. Na produkcji Traefik, nginx lub Cloudflare przekazują jej zwykłe HTTP na wewnętrznym porcie. Ten sam kod, inny odtworzony URL, więc podpis przestaje się zgadzać. To przyczyna numer jeden na tej stronie.

Czy mój Auth Token jest wysyłany na Wasze serwery?

Nie. HMAC liczy się w Twojej przeglądarce za pomocą WebCrypto, a narzędzie nie wysyła żadnego żądania, gdy z niego korzystasz. Otwórz zakładkę sieci i sprawdź. Twój Auth Token to produkcyjny sekret — zadawaj to pytanie każdej stronie, która o niego prosi, także naszej.

Czy podpis Twilio to SHA-256, jak w Stripe?

Nie, Twilio używa HMAC-SHA1 i koduje wynik w base64. Stripe, GitHub i Shopify używają SHA-256. Przerobienie działającego handlera Stripe przez zmianę nazwy nagłówka daje walidator, który odrzuca wszystko — to częsta droga do tego błędu.

Oferta startowa · −50 %

Debugujesz integrację webhooków w tym tygodniu?

Pass na 7 dni odblokowuje wyjaśnienia AI i gotowy kod handlera, 25 stałych URL-i, 30 dni historii i tunel Relay do localhost. Jedna płatność, bez subskrypcji.

5 € 2,50 € jednorazowo
Płatność jednorazowa · bez subskrypcji · zwrot w ciągu 14 dni · cena startowa do 30 listopada
Walidacja podpisu Twilio nie powiodła się: znajdź przyczynę · Webhook Toolkit