Приём вебхуков без двойных списаний
webhook-handlerFREEвебхукиплатежи
Подпись, повторы и идемпотентность — четыре правила, из-за которых один платёж не продлевает доступ дважды.
Что делает
- Срабатывает на «добавь обработчик уведомлений», «подключи платежи», «пришло дважды».
- Исходит из того, что отправитель пришлёт одно и то же событие несколько раз, не по порядку и иногда сильно позже.
- Требует проверять подпись по сырым байтам, а не по разобранному и заново собранному JSON: пересборка меняет порядок ключей, и подпись перестаёт сходиться по причине, которую ищут неделю.
- Хранит идентификатор события с уникальным ограничением и делает проверку и работу одной транзакцией — двумя отдельными запросами они гонятся, а под повторами гонятся обязательно.
- Отвечает двухсотым как можно раньше и работает после: работа до ответа съедает таймаут отправителя, а таймаут — это ещё один повтор.
- Требует отвечать двухсотым и на события, которые вы не обрабатываете: четырёхсотый заставляет отправителя повторять их вечно и в итоге отключить адрес.
- В логах при неудачной подписи оставляет только идентификатор события: ни тела, ни заголовка, ни вычисленной подписи.
Зачем он нужен
Уведомление приходит на каждую смену статуса, и провайдер повторяет его, пока не увидит успешный ответ. Без ключа по идентификатору события один платёж продлевает подписку дважды, а человек об этом даже не узнает — узнает бухгалтерия через месяц. Проверка подписи по пересобранному JSON и работа до ответа — две другие ошибки, которые повторяют почти все.
Куда положить
- 1Создайте в проекте папку .claude/skills/webhook-handler
- 2Положите в неё файл SKILL.md с текстом ниже
- 3Всё. Claude Code подключит скил сам, когда задача подойдёт под описание
Чтобы скил работал во всех проектах, а не в одном, положите его в ~/.claude/skills вместо папки проекта.
Файл SKILL.md
--- name: webhook-handler description: Receive webhooks from a payment provider or external service correctly. Use when the user adds a callback endpoint, integrates payments, or reports that something was processed twice. --- # Receiving a webhook The sender will deliver the same event more than once, out of order, and sometimes months late. It will retry until you answer 200. Everything below follows from that. ## The four rules **1. Verify the signature before reading the body.** Compute it over the raw bytes, not over the parsed and re-serialised JSON — reserialising changes key order and whitespace, and the signature stops matching for reasons nobody finds quickly. Compare in constant time. An unsigned endpoint that grants access is a free access endpoint for anyone who reads your docs. **2. Be idempotent.** Store the provider's event id, unique-constrained, and check it before acting. Do the check and the work in one transaction — two separate statements race, and under retries they will race, because retries arrive in bursts. This is what stops one payment extending a subscription twice. **3. Answer fast, work after.** Return 200 as soon as the event is stored. Work done before the response counts against the sender's timeout, and a timeout means a retry, which means the same work again. **4. Answer 200 for events you ignore.** A 4xx to an event type you do not handle makes the sender retry it forever and eventually disable the endpoint. ## What to log and what never to log Log the event id, the type and the outcome. On a failed signature check, log the event id and nothing else — not the body, not the header, not the computed digest. Those end up in a log aggregator that more people can read than you think. Never log the secret. Never accept it from a query parameter. ## Test it - Send the same event twice and check the effect happened once. - Send it with a wrong signature and check nothing happened. - Send an unknown event type and check the answer is 200. - Send an event for an object that does not exist locally and check the handler does not crash the endpoint for every subsequent event. ## Before finishing State plainly what happens if the endpoint is down for an hour. If the answer is "those events are lost", say it — most providers retry for days, but only if you answered with an error rather than a 200.