Вебхук об оплатах: настройка и формат
Адрес, секрет, подпись, формат JSON, повторы доставки и проверка подписи на вашем сервере.
Включение
- 1Откройте «Настройки школы → Вебхук об оплатах»Нужно право «Управление настройками».
- 2Укажите адрес приёмника и нажмите «Сохранить»Только публичный https-адрес. Локальные и внутренние адреса не принимаются.
- 3Скопируйте секрет подписиОн показывается один раз. Потеряли — нажмите «Выпустить новый секрет» (старый перестанет действовать).
- 4Нажмите «Отправить тест», затем включите тумблерТест пришлёт событие test с вымышленным заказом — убедитесь, что ваш сервер ответил 2xx.
Как приходит запрос
- POST на ваш адрес, тело — JSON (UTF-8), заголовок Content-Type: application/json.
- X-1Study-Event — тип события: payment.succeeded, payment.refunded или test.
- X-1Study-Delivery — уникальный id доставки (совпадает с полем id в теле). По нему удобно отсекать повторы.
- X-1Study-Signature — подпись: t=<unix-время>,v1=<HMAC-SHA256 от строки "<t>.<тело>" с вашим секретом, hex>.
- Ответ 2xx в течение 10 секунд = доставлено. Иначе повторим через 1 мин, 5 мин, 30 мин, 2 ч и 12 ч (всего 6 попыток), после чего доставка помечается как неудачная. Журнал последних доставок виден в настройках.
Пример тела payment.succeeded
{
"id": "0f6c1d2e-…",
"event": "payment.succeeded",
"created_at": "2026-10-15T10:21:08.512Z",
"test": false,
"school": { "id": "c7065db2-…", "username": "school" },
"order": {
"id": "a1b2c3d4-…",
"status": "granted",
"amount": 4990,
"currency": "KZT",
"refunded_amount": 0,
"created_at": "2026-10-15T10:19:40.000Z",
"payment_method": "kaspi",
"source": "guest-checkout",
"quantity": 1,
"item": { "type": "course", "id": "ca86dbbd-…", "title": "Говори Дорого" },
"buyer": { "client_id": "…", "name": "Айгерим Ахметова", "phone": "+77010000000", "email": null },
"attribution": {
"ref": "blogger1",
"utm_source": "instagram",
"utm_medium": "blogger",
"landing": "/school",
"capturedAt": "2026-10-12T18:03:11.000Z"
}
}
}При возврате приходит payment.refunded с тем же order.id: в refunded_amount — возвращённая сумма, в status — refunded (полный) или partially_refunded (частичный). Если покупатель пришёл без размеченной ссылки, attribution будет null. item.type — course, product или ticket.
Проверка подписи
Проверяйте подпись по сырому телу запроса (до разбора JSON) и отбрасывайте события старше 5 минут — это защита от подделки и повторной отправки.
// Node.js (Express): app.post('/1study/webhook', express.raw({ type: 'application/json' }), handler)
const crypto = require('crypto');
function verify(req, secret) {
const header = req.get('X-1Study-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + req.body.toString('utf8')).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && parts.v1 && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}<?php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_1STUDY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'] . '.' . $raw, $secret);
$ok = abs(time() - (int)$parts['t']) < 300 && hash_equals($expected, $parts['v1'] ?? '');Внимание
Одно и то же событие может прийти повторно (например, если ваш сервер ответил ошибкой, а потом обработал запрос). Храните id доставки и игнорируйте уже обработанные.
Совет
Для быстрой проверки без своего сервера можно временно указать адрес https://api.1app.kz/api/webhooks/echo — он отвечает 200 и ничего не сохраняет. Потом замените на свой.