Handlery webhooków, które przetrwają produkcję
Czym jest dostawa webhooka cmssy, jak zweryfikować jej podpis, co robi drabina ponowień i jak napisać handler, który można bezpiecznie uruchomić dwa razy.
Handler webhooków wygląda jak dziesięć linijek kodu - aż do pierwszego razu, gdy zaksięguje płatność dwa razy, zgubi połowę dostaw podczas rotacji sekretu albo po nieudanym deployu po cichu przestanie cokolwiek odbierać. Nic z tego nie jest egzotyczne. Tak kończy się handler, który zakłada, że dostawa przychodzi raz, po kolei i podpisana jedynym sekretem, jaki zna.
Ten wpis to checklista handlera webhooków cmssy, który przetrwa produkcję: czym jest dostawa, jak ją zweryfikować, co robi drabina ponowień i jak napisać handler, który można bezpiecznie uruchomić dwa razy.
Czym jest dostawa
Każda dostawa to POST z treścią JSON:
{
"id": "0b6f2c1e-…",
"event": "order.paid",
"createdAt": "2026-10-26T14:21:19.846Z",
"data": { "workspaceId": "…", "order": { … } }
}id to id dostawy. Pozostaje takie samo przy każdym ponowieniu tej dostawy i przychodzi też w nagłówku x-cmssy-webhook-id, obok x-cmssy-event i x-cmssy-signature. Ponowne wysłanie z panelu admina lub przez redeliver_webhook_delivery tworzy nową dostawę z nowym id, więc handler deduplikujący po id traktuje ręczne powtórzenie jako nowe - a zwykle właśnie tego chcesz, gdy powtarzasz celowo.
Payloady są oszczędne: zdarzenia treści niosą referencje, nie treść, a form.submitted niesie id zgłoszenia, nie odpowiedzi. Zdarzenia commerce niosą migawkę zamówienia, której potrzebuje Twoja integracja - status, sumy, traktowanie podatkowe, kupującego, paczki i faktury.
Zweryfikuj podpis

Nagłówek ma postać t=<unix-ms>,v1=<hex>. Hex to HMAC-SHA256 z <t>.<raw body>, z sekretem endpointu jako kluczem. Na trzech szczegółach wykładają się ręcznie pisane weryfikatory:
- Rotacja wysyła dwa podpisy. Podczas rotacji sekretu nagłówek niesie jedno
v1na każdy aktywny sekret. Weryfikator, który sprawdza tylko ostatnie, odrzuca każdą dostawę podpisaną drugim - przez cały czas trwania rotacji. - Treść musi być surowa. Sparsuj JSON i zserializuj go ponownie, a bajty się zmienią - kolejność kluczy, białe znaki, escape'y unicode. Weryfikuj
await request.text(), a dopiero potem parsuj. - Stare znaczniki czasu muszą być odrzucane. Bez okna tolerancji przechwyconą dostawę można odtwarzać w nieskończoność.
Helper z SDK robi wszystkie trzy rzeczy: czyta każde v1, porównuje w stałym czasie, odrzuca znacznik czasu starszy niż pięć minut i zwraca typowane zdarzenie. Jest w @cmssy/next, @cmssy/remix i @cmssy/astro, a dla całej reszty w @cmssy/core.
import { verifyCmssyWebhook, CmssyWebhookError } from "@cmssy/next";
export async function POST(request: Request) {
const body = await request.text();
try {
const event = await verifyCmssyWebhook({
body,
signatureHeader: request.headers.get("x-cmssy-signature"),
secret: [process.env.CMSSY_WEBHOOK_SECRET, process.env.CMSSY_WEBHOOK_SECRET_PREVIOUS]
.filter((s): s is string => !!s),
});
await handle(event);
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof CmssyWebhookError) {
return new Response(null, { status: 400 });
}
throw error;
}
}Opcja secret przyjmuje tablicę, więc Twój handler może trzymać nowy i poprzedni sekret w Twoim własnym oknie rotacji i akceptować dostawy podpisane którymkolwiek z nich.
Drabina ponowień

cmssy czeka na odpowiedź 15 sekund. Wszystko poza 2xx - i każdy timeout - jest ponawiane: łącznie osiem prób, z odstępami 1 minuta, 5 minut, 30 minut, 2 godziny, 5 godzin, 10 godzin i 10 godzin. Handler, który leży całe popołudnie, nic nie traci.
- Przekierowania to porażki. 3xx nie jest śledzone. Zarejestruj docelowy URL.
- Tylko 410 Gone zatrzymuje drabinę. Każde inne 4xx jest ponawiane, bo „ten endpoint skończył się na dobre” i „deploy, do którego należy ta trasa, przez chwilę odpowiada 404” z zewnątrz wyglądają tak samo. Zwracaj 410, gdy naprawdę o to chodzi.
- Retry-After jest respektowany, gdy prosi o dłuższe czekanie niż drabina, maksymalnie do 10 godzin. Nigdy nie skraca odstępu.
- Dwadzieścia porażek z rzędu wyłącza endpoint. Napraw handler, a potem włącz go ponownie; to zeruje licznik.
Dostawy są kolejkowane per endpoint, z jedną próbą w toku naraz, więc wolny handler kolejkuje własne ponowienia, zamiast być zasypywany przez fan-out.
Napisz handler, który można bezpiecznie uruchomić dwa razy
Dostawa at-least-once oznacza, że Twój handler prędzej czy później zobaczy tę samą dostawę dwa razy - timeout po Twojej stronie, który cmssy policzyło jako porażkę, deploy, który odpowiedział 502 już po wykonaniu pracy. A ponieważ dostawy nie są uporządkowane, ponowienie może dotrzeć po nowszym zdarzeniu dotyczącym tego samego zamówienia.
- Potwierdzaj szybko, pracuj potem. Zweryfikuj, zapisz dostawę, zwróć 2xx. Handler, który przed odpowiedzią woła trzy API, w końcu przekroczy 15 sekund i zostanie ponowiony, gdy wciąż pracuje.
- Deduplikuj po id dostawy. Unikalny indeks na
idw Twojej własnej tabeli zamienia duplikat w no-op. - Przed efektami ubocznymi czytaj aktualny stan. Payload to migawka z chwili zdarzenia. Gdy to ma znaczenie - wystawienie faktury, wysyłka e-maila, przesunięcie stanów w innym systemie - odczytaj zamówienie ponownie i działaj na tym, co jest prawdą teraz.
- Nigdy nie opieraj się na kolejności przyjścia. Porównuj stan z payloadu albo ze świeżego odczytu z tym, co już zastosowałeś.
Gdy coś jednak pójdzie nie tak
Każda próba jest logowana: list_webhook_deliveries (lub Settings → Webhooks w panelu admina) pokazuje status, kod odpowiedzi, a członkom zespołu z webhooks:manage pierwsze 8 KiB treści odpowiedzi Twojego handlera - zwykle ten stack trace, którego szukałeś. Gdy handler jest naprawiony, wyślij ponownie nieudane dostawy. Ponowne wysłanie to świeża dostawa z własnym id; nie liczy się do drabiny oryginału ani do serii porażek endpointu.
Jedna subskrypcja czy wiele?
Do unieważniania cache zasubskrybuj raz content.changed i unieważniaj przy każdej zmianie; obejmuje strony, rekordy, modele, formularze, media i ustawienia. Do automatyzacji subskrybuj szczegółowe zdarzenia, których potrzebujesz - order.*, shipment.*, page.published, form.submitted i resztę. list_webhook_event_types zwraca pełną listę dozwolonych zdarzeń, przefiltrowaną według tego, co może subskrybować Twoja rola.
Pełna dokumentacja jest w dokumentacji webhooków. Działający przykład idempotentnego handlera znajdziesz we wpisie fakturowanie headless sklepu z webhookami.
