Перейти к содержимому

Приём и проверка вебхуков

Каждый раз, когда платёж, возврат или выплата меняет статус, мы отправляем событие на ваш эндпоинт. События подписаны по схеме Standard Webhooks, поэтому вы проверяете их готовой библиотекой — свой код подписи не нужен.

Один HTTPS-эндпоинт на проект: публичный хост с именем, порт 443 или 8443. IP-адрес, другой порт, имя пользователя в URL или фрагмент — это 422 url_not_allowed.

examples/curl/webhooks/endpoint/set.sh
#!/usr/bin/env bash
# Point events at your HTTPS address. Keep the secret of the first answer: it is shown once.
set -euo pipefail
. "$(dirname "$0")/../../signed-request.sh"
body=$(cat <<JSON
{
"url": "${WEBHOOK_URL:-https://shop.example.lk/webhooks/processing}"
}
JSON
)
mka_request PUT "/v1/webhook-endpoint" "$body"

Первый ответ, 201, содержит секрет подписи whsec_… — один раз. Храните его вместе с другими секретами. Если позже сменить адрес, ответ будет 200, а секрет останется прежним. Потеряли секрет? Смените его.

examples/node/webhooks/receive.mjs
// Receive webhooks: verify each one with the Standard Webhooks library, act on it once.
import { createServer } from "node:http";
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…, answered once by PUT /v1/webhook-endpoint
const handled = new Set(); // webhook-id of the events acted on; keep this in your database
const versions = new Map(); // the highest version applied, per payment, refund or payout
createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
let event;
try {
// The exact bytes received, before any JSON parsing; the timestamp must be within 5 minutes.
event = webhook.verify(Buffer.concat(chunks).toString("utf8"), req.headers);
} catch {
res.writeHead(400).end();
return;
}
if (!handled.has(req.headers["webhook-id"])) {
handled.add(req.headers["webhook-id"]);
apply(event);
}
res.writeHead(204).end(); // answer 2xx within 15 seconds; do slow work after answering
});
}).listen(Number(process.env.PORT ?? 8080));
function apply(event) {
const [subject, version] = {
"payment.status_changed": [event.payment_id, event.payment_version],
"refund.status_changed": [event.refund_id, event.refund_version],
"payout.status_changed": [event.payout_id, event.payout_version],
}[event.type] ?? [event.event_id, 0];
// Events may come late or twice: an older version than the one applied changes nothing.
if ((versions.get(subject) ?? 0) > version) return;
versions.set(subject, version);
console.log(`verified ${event.type} ${subject} v${version} ${event.status ?? ""}`);
}
  1. Проверяйте подпись по сырому телу, до разбора: подпись покрывает точные байты. Фреймворк, который сначала разбирает JSON, а потом сериализует его заново, ломает подпись.
  2. Отклоняйте устаревшую метку времени. Библиотеки отказывают, если расхождение больше 5 минут; оставьте так.
  3. Отвечайте 2xx в течение 15 секунд, а медленную работу делайте потом. Всё остальное — таймаут, редирект, 5xx — повторяется.
  4. Обрабатывайте каждый webhook-id один раз: повтор несёт тот же id (порядок).

У каждого события три заголовка:

Заголовок
webhook-id Id события; одинаковый при каждом повторе.
webhook-timestamp Время этой попытки в секундах Unix.
webhook-signature v1, и HMAC-SHA256 в base64 от id.timestamp.body с вашим секретом. Пока при смене секрета действуют оба, по одной подписи на секрет через пробел.
Тип Когда Ключевые поля
payment.status_changed Платёж перешёл в requires_action, processing, succeeded, failed или cancelled. payment_id, payment_version, status, data.merchant_order_id, data.amount, data.amount_received
refund.status_changed Возврат запрошен, взят, отправлен или не прошёл. refund_id, refund_version, payment_id, status
payout.status_changed Выплата создана, взята, отправлена, не прошла или отменена. payout_id, payout_version, status, data.merchant_payout_id, data.fee
webhook.test Вы запросили тест. —

Все поля описаны в справочнике событий. Игнорируйте незнакомые типы событий и поля: новые добавляются в рамках v1.

examples/curl/webhooks/endpoint/send-test-event.sh
#!/usr/bin/env bash
# Send a webhook.test event now and see how your endpoint answered.
set -euo pipefail
. "$(dirname "$0")/../../signed-request.sh"
mka_request POST "/v1/webhook-endpoint/test"

Ответ показывает, принял ли эндпоинт событие и с каким статусом. Все события и попытки также видны в консоли в разделе «Разработчикам» → «Вебхуки» и доступны через API:

examples/curl/webhooks/deliveries/list.sh
#!/usr/bin/env bash
# Your deliveries, newest first, 50 a page.
set -euo pipefail
. "$(dirname "$0")/../../signed-request.sh"
mka_request GET "/v1/webhook-deliveries"