Webhook-уведомления Ketmus: события, подпись и повторные доставки
Какие события присылает Ketmus, как проверить подпись HMAC-SHA512 и почему ваш сервер должен отвечать 2xx.

Когда вы передаёте callback_url при создании счёта, Ketmus отправляет на этот адрес POST-запрос при каждом изменении статуса. Тело запроса — JSON счёта в том же формате, что и в ответе API, плюс поле event.
События
- invoice.pending, invoice.confirming, invoice.paid, invoice.paid_over, invoice.wrong_amount, invoice.expired, invoice.cancelled — повторяют смену статуса счёта.
- invoice.late_payment — перевод пришёл уже после истечения срока счёта.
- static.payment — поступление на статический адрес клиента; в теле дополнительно customer_id и static_address.
- webhook.test — тестовое событие, которое можно отправить кнопкой из кабинета, чтобы проверить приёмник.
Заголовки и подпись
Каждый запрос содержит заголовки x-ketmus-event, x-ketmus-timestamp и x-ketmus-signature. Подпись — HMAC-SHA512 от строки «timestamp.body» с секретом проекта из кабинета. Проверяйте её по сырому телу запроса до разбора JSON и отклоняйте события старше 5 минут.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyKetmus(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
if (!parts.t || !parts.v1 || age > 300) return false
const expected = createHmac('sha512', secret).update(`${parts.t}.${rawBody}`).digest('hex')
return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
Для совместимости со старыми интеграциями те же значения дублируются в заголовках x-platupay-*. Новым интеграциям следует читать только x-ketmus-*.
Повторные доставки
Ваш сервер должен ответить любым кодом 2xx в течение 10 секунд. Иначе Ketmus повторит доставку через 1, 5 и 30 минут, затем через 2, 6 и 24 часа. Поэтому обработчик должен быть идемпотентным: одно и то же событие может прийти дважды — сверяйтесь с id счёта и статусом, который вы уже сохранили.
Не доверяйте webhook как единственному источнику истины для выдачи товара: подтверждайте статус запросом GET /api/merchant/v1/invoices/{id} или проверяйте подпись, как показано выше. Любой из этих способов исключает подделку уведомления.

