9. Вебхуки
Вебхук — это HTTP-запрос от ShiftPay на ваш сервер, когда событие по платежу произошло. Он нужен, чтобы вы не опрашивали API.
9. Вебхуки
Вебхук — это HTTP-запрос от ShiftPay на ваш сервер, когда событие по платежу произошло. Он нужен, чтобы вы не опрашивали API.
9.1. Как подключить
**Подключение → Вебхуки** (или «Подключение магазина» → «Настроить вебхуки»).
Нажмите **«Добавить адрес»** (кнопка «Создать вебхук»).
Введите **HTTPS-адрес** вашего обработчика. Требования:
- публичный HTTPS на порту 443 с действительным сертификатом; - без логина и пароля в URL, без фрагментов (`#…`) и без перенаправлений; - домен, а не IP; локальные адреса не допускаются.
Подтвердите действие. Адрес **сразу включается**: новые события магазина идут на него.
Вы получите **секрет подписи вебхука** — он показывается **один раз**. Сохраните его на сервере. Это **не** секрет API-ключа. Если секрет потерян, остановите эту версию адреса и создайте новый адрес.
Подключайте адрес **до создания платежей**: события, возникшие до подключения, автоматически не досылаются.
Версии адреса: смена адреса создаёт новую версию. Старые события сохраняют исходный адрес и секрет; предыдущая версия доработает свою очередь. Кнопка **«Остановить»** прекращает новые попытки для версии (уже идущий HTTP-запрос может завершиться; старые события на другой адрес не переносятся). В списке показываются последние версии адреса (до 50).
9.2. Что приходит
`POST` на ваш адрес, тело — JSON (UTF-8). Заголовки:
| Заголовок | Значение | | --- | --- | | `Content-Type` | `application/json` | | `User-Agent` | `ShiftPay-Webhooks/1` | | `ShiftPay-Event-ID` | Уникальный ID события (повторы приходят с тем же ID). | | `ShiftPay-Event` | Тип события, например `payment.succeeded`. | | `ShiftPay-Timestamp` | Unix-время отправки. | | `ShiftPay-Signature` | `v1=<hex>` HMAC-SHA256. |
Конверт тела: объект с полями `id` (ID события), `type` (тип), `created_at`, `livemode` и `data` с данными платежа (те же поля, что в ответе `GET /payments/{id}`: `id`, `status`, `amount`, `currency`, `payer_total`, `merchant_amount`, `fee`, `merchant_order_id`, `metadata` и т. д.). Детали состава `data` для конкретного типа события смотрите в журнале доставки и в `GET /payments/{id}/events`.
Основные типы событий платежа: `payment.created`, `payment.succeeded` (платёж оплачен, статус `paid`), `payment.cancelled`, `payment.confirming`, `payment.aml_hold`. Полный список для вашего магазина удобнее всего посмотреть в журнале событий платежа. Относитесь к списку как к расширяемому: **игнорируйте неизвестные типы, отвечая 2xx**.
9.3. Проверка подписи
Подпись: `HMAC-SHA256(секрет_вебхука, timestamp + "." + сырое_тело)`, hex, с префиксом `v1=`.
Правила:
Считайте подпись по **исходным байтам тела** до JSON-разбора.
Отклоняйте запрос, если `ShiftPay-Timestamp` старше 5 минут.
Сравнивайте подписи в постоянное время.
Дедуплицируйте по `ShiftPay-Event-ID`.
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody — Buffer с исходными байтами тела; headers — Headers (или аналог).
export function verifyWebhook(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || !secret) return false;
const timestamp = headers.get("ShiftPay-Timestamp") ?? "";
const signature = headers.get("ShiftPay-Signature") ?? "";
if (!/^\d{10}$/.test(timestamp) || !/^v1=[a-fA-F0-9]{64}$/.test(signature)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest();
const received = Buffer.from(signature.slice(3), "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}Python:
import hashlib, hmac, time
def verify_webhook(raw_body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("ShiftPay-Timestamp", "")
signature = headers.get("ShiftPay-Signature", "")
if not (timestamp.isdigit() and len(timestamp) == 10 and signature.startswith("v1=") and len(signature) == 67):
return False
if abs(int(time.time()) - int(timestamp)) > 300:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature[3:])9.4. Что делать в обработчике
Проверьте подпись.
Разберите JSON.
Сопоставьте `data.id`, магазин, валюту и сумму с вашим заказом.
В **одной транзакции** сохраните обработанный `ShiftPay-Event-ID` и изменение заказа.
Верните **2xx** только после успешного сохранения.
Повтор уже обработанного события должен безопасно возвращать 2xx без повторной выдачи товара.
Ответьте в пределах **10 секунд**. HTTP 2xx подтверждает **приём** запроса, а не выполнение логики на вашей стороне. Доставка — минимум один раз: возможны повторы и другой порядок событий. Для критичных решений дополнительно сверяйтесь с `GET /payments/{id}`.
9.5. Повторы
При ошибке ShiftPay повторяет доставку через 10, 30, 60, 90, 180, 360, 480, 720, 1080, 1440, 2160, 2880 и 4320 минут от первой попытки (пропущенные интервалы не выполняются одновременно). После исчерпания попыток нужен разбор ошибки и повтор из кабинета. При повторе ID события и байты тела прежние, а `timestamp` и подпись обновляются.
9.6. Журнал доставки и ручной повтор
В карточке платежа (раздел «Платежи») есть **журнал доставки**: ID доставки, адрес, очередь, попытки, время следующей попытки, первая доставка, последний результат, запрос (очищенные данные) и ответ (безопасное описание), более ранние попытки.
Кнопка **«Повторить вебхук»**: нужен свежий вход с MFA; не чаще одного повтора в минуту для одной доставки. Повтор использует исходные ID события, тело, адрес и секрет, **не создаёт платёж и не зачисляет деньги**. Если связь прервалась, можно «Продолжить запрос повтора» с прежним ключом, повторная постановка не создаст дубликат.
Если у события нет записи доставки, оно, возможно, возникло до подключения адреса.
---
Страница была полезна?
