Документация
1. Публикация события
curl -X POST https://hookpulse.altixcode.com/api/v1/apps/APP_ID/events \
-H "Authorization: Bearer hp_…" \
-H "Content-Type: application/json" \
-d '{
"type": "invoice.paid",
"payload": { "id": "in_123", "amount": 4900 },
"dedupeKey": "invoice-123-paid"
}'Ответ возвращается сразу после того, как событие сохранено и поставлено в очередь. Передайте dedupeKey, и повторный запуск из вашего обработчика задач не приведет к дублированию отправки.
2. Что получает ваш клиент
POST /their/endpoint
Content-Type: application/json
HookPulse-Signature: t=1700000000,v1=<base64 hmac>
HookPulse-Delivery-Id: dlv_…
HookPulse-Timestamp: 1700000000
{"id":"evt_…","type":"invoice.paid","createdAt":"…","data":{…}}3. Проверка подписи
Предоставьте это вашим клиентам. Две самые распространенные ошибки — использовать обычное сравнение через === и игнорировать метку времени; обе приводят к незаметным уязвимостям.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(headers, rawBody, secret, toleranceSeconds = 300) {
const header = headers['hookpulse-signature']
const deliveryId = headers['hookpulse-delivery-id']
const timestamp = Number(/t=(\d+)/.exec(header)?.[1])
// Replay protection. Without this a captured request is valid forever.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = createHmac('sha256', secret)
.update(`${deliveryId}.${timestamp}.${rawBody}`)
.digest('base64')
return [...header.matchAll(/v1=([^,]+)/g)].some(([, presented]) => {
const a = Buffer.from(presented)
const b = Buffer.from(expected)
return a.length === b.length && timingSafeEqual(a, b)
})
}Проверяйте подпись по исходному (raw) телу запроса до парсинга JSON. Повторная сериализация меняет байты, из-за чего любая подпись перестает сходиться.
4. Ротация секретного ключа
При ротации создается новый секрет, а старый остается действительным на заданный вами период. Мы сразу начинаем подписывать новым ключом, побуждая клиентов обновиться, но эндпоинты, все еще проверяющие по старому секрету, продолжат работать до закрытия окна перехода.
5. Лог доставок
Сгенерируйте токен портала на своем бэкенде (по аналогии с подписью вебхука, с указанием конкретного клиента) и вызывайте API портала прямо со страницы настроек в вашем приложении. Каждый запрос изолирован по подписчику из токена, поэтому один клиент никогда не сможет увидеть данные другого.
GET https://hookpulse.altixcode.com/api/portal/APP_ID/deliveries
POST https://hookpulse.altixcode.com/api/portal/APP_ID/deliveries/DELIVERY_ID/retry
Authorization: Bearer <portal token>Или встройте его
На тарифах Pro и Scale просто добавьте <hookpulse-log> на страницу настроек вместо создания собственной таблицы. Тот же токен, те же ограничения доступа — компонент сам отобразит журнал в shadow root, поэтому стили вашей страницы не будут с ним конфликтовать.
<script src="https://hookpulse.altixcode.com/embed/hookpulse-log.js"></script>
<hookpulse-log app-id="APP_ID" token="PORTAL_TOKEN" api-url="https://hookpulse.altixcode.com"></hookpulse-log>Честные ограничения
- Очередь хранится в таблице PostgreSQL. Благодаря этому журнал доставок и очередь всегда согласованы, а при перезапуске ничего не теряется. Это не самое быстрое архитектурное решение, и при очень высоких нагрузках брокер сообщений справился бы лучше.
- Воркер работает в том же веб-процессе. Механизм захвата задач уже безопасен для запуска нескольких воркеров, поэтому их вынос — это вопрос конфигурации деплоя, а не переписывания кода. Однако сейчас нагрузка по доставке и обработка запросов делят один процесс.
- Мы не следуем редиректам. Переход по перенаправлению привёл бы к отправке подписанных данных на хост, который вы не указывали. Коды 3xx считаются ошибкой доставки.
- Эндпоинт отключается после 20 ошибок подряд. Один неработающий URL не должен тратить попытки, нужные остальным.