Мы сообщаем о завершении платежа HTTP-запросом на ваш адрес. Адрес задаётся в боте, в разделе «API для бизнеса».
https и публично доступный хост;2xx в течение 10 секунд — тело ответа мы не читаем;| Событие | Когда |
|---|---|
payment.succeeded | Платёж прошёл, списание окончательное |
payment.failed | Платёж не прошёл, USDT вернулись на баланс |
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-ExpressPay-Event: payment.succeeded
X-ExpressPay-Delivery: whd_3f1c9a2e7b444c1e
X-ExpressPay-Payment-Id: pay_9f3c1a2b4d5e
X-ExpressPay-Signature: t=1789483018,v1=5f2a...c91
{
"id": "evt_1789483018420",
"event": "payment.succeeded",
"created_at": "2026-09-15T10:22:48+00:00",
"data": { ...объект платежа, как в GET /v1/payments/{id}... }
}
Подпись считается по строке "{t}.{сырое тело запроса}" алгоритмом
HMAC-SHA256 на вашем секрете вебхуков (виден в боте). Сравнивайте подписи функцией
постоянного времени и отбрасывайте запросы старше пяти минут — метка времени внутри
подписи защищает от повторной отправки перехваченного запроса.
function verify(string $body, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $parts); // t=…, v1=…
$t = $parts['t'] ?? '';
if (abs(time() - (int)$t) > 300) return false;
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}
import hashlib, hmac, time
def verify(body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(
secret.encode(), f'{parts["t"]}.'.encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
import crypto from "node:crypto";
function verify(body, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${body}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Если ваш сервер не ответил 2xx, мы повторяем доставку девять раз с
нарастающими паузами: сразу, через 10 и 30 секунд, 2, 5 и 15 минут, 1, 3 и 6 часов.
После этого доставка помечается неудачной, а мы пишем вам в бот.
Если подряд не доставляются три события, отправка вебхуков приостанавливается, чтобы
не копить очередь в никуда. Статусы в это время по-прежнему доступны через
GET /v1/payments/{id}. Включить обратно — кнопкой «Проверить вебхук» в боте.
payment.id + status и делайте обработку идемпотентной.GET /v1/payments/{id}.200;
долгая обработка внутри запроса приведёт к таймауту и лишним повторам.