Вебхуки
События заказов и аккаунтов, подпись HMAC-SHA256, повторы.
Вебхук это POST на URL вашего Project при каждом событии. Endpoint и секрет подписи задаются в кабинете; секрет показывается один раз.
События
type | Когда |
|---|---|
order.succeeded | заказ выполнен |
order.failed | заказ не выполнен, блокировка на балансе снята |
order.needs_review | исход неизвестен, заказ проверяет поддержка |
supplier_account.health_changed | изменилось здоровье вашего Supplier Account (режим OWN) |
Формат
POST /ifragment/webhook HTTP/1.1
Content-Type: application/json
X-IFragment-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd{
"id": "0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b",
"type": "order.succeeded",
"createdAt": "2026-09-26T10:15:30.000Z",
"data": {
"id": "0199a1b2-6f00-7abc-9def-112233445566",
"externalId": "1042",
"product": "STARS",
"quantity": 500,
"months": null,
"username": "durov",
"recipientName": "Pavel Durov",
"status": "SUCCEEDED",
"price": "8.25",
"currency": "USD",
"failure": null,
"createdAt": "2026-09-26T10:15:02.000Z",
"sentAt": "2026-09-26T10:15:05.000Z",
"completedAt": "2026-09-26T10:15:30.000Z"
}
}Для supplier_account.health_changed в data приходит аккаунт:
{
"accountId": "0199a1b2-1111-7abc-9def-112233445566",
"label": "Main wallet",
"previousHealth": "HEALTHY",
"health": "DEGRADED",
"reasons": ["LOW_BALANCE"],
"changedAt": "2026-09-26T10:20:00.000Z"
}idэто id события, он один и тот же при повторных доставках. Храните обработанныеidи пропускайте дубли: доставка работает по принципу «как минимум один раз».dataсодержит объект заказа (как вGET /v1/orders/{id}) или аккаунта дляsupplier_account.health_changed. Уorder.failedпричина лежит вdata.failure.code.- События могут прийти не по порядку; ориентируйтесь на
data.status, а при сомнениях запросите заказ через API.
Ответ
Ответьте любым 2xx за 10 секунд, тело ответа не читается. Долгую обработку делайте после ответа (очередь, фоновая задача).
Повторы
При ответе не 2xx, таймауте или ошибке соединения доставка повторяется с экспоненциальной задержкой: всего 12 попыток примерно за 24 часа. После этого доставка помечается FAILED; повторить её можно вручную из кабинета.
Проверка подписи
X-IFragment-Signature: t=<unix>,v1=<hex>, где v1 = hex(HMAC-SHA256(secret, "<t>.<rawBody>")):
- разберите заголовок на
tиv1; - посчитайте HMAC-SHA256 от строки
t + "." + сырое тело запроса, то есть байты как пришли, до парсинга JSON; - сравните с
v1за постоянное время; - отклоните запрос, если
tотличается от текущего времени больше чем на 5 минут (защита от повторного воспроизведения).
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyIFragmentSignature(rawBody: string, header: string, secret: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=', 2) as [string, string]));
const timestamp = Number(parts.t);
if (!parts.v1 || !Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) {
return false;
}
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}
// Express: берите сырое тело, а не распарсенный JSON.
app.post('/ifragment/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyIFragmentSignature(raw, req.get('X-IFragment-Signature') ?? '', process.env.IFRAGMENT_WEBHOOK_SECRET!)) {
return res.sendStatus(401);
}
const event = JSON.parse(raw);
res.sendStatus(200);
queue.add(event); // обработка после ответа
});Частая ошибка: считать подпись от заново сериализованного JSON (JSON.stringify(req.body)). Порядок полей и пробелы могут отличаться, и подпись не сойдётся. Всегда используйте сырое тело.