Уведомления
Чтобы отслеживать состояние платежей и возвратов, подпишитесь на уведомления (webhook). Уведомление — POST-запрос от AvantPay на ваш сервер, когда объект API меняется без вашего участия: например, покупатель оплатил платёж. Вместо того чтобы периодически запрашивать статус, дождитесь уведомления.
События
Событие — изменение статуса объекта. Название события формируется по шаблону <объект>.<статус>: например, payment.succeeded — платёж перешёл в статус succeeded.
| Событие | Когда приходит | object |
|---|---|---|
payment.succeeded | Платёж оплачен. | Объект платежа |
payment.canceled | Платёж отменён: истёк срок оплаты, банк отказал или сработала проверка. | Объект платежа |
refund.succeeded | Возврат выполнен — полный или частичный, созданный через API или в личном кабинете. | Объект возврата |
payment_method.active | Способ оплаты сохранён для автоплатежей. | Объект способа оплаты |
Уведомления об отмене возврата нет: отмену видно в ответе на GET /v3/refunds/{refund_id}.
Настройка
URL и события задаются только в личном кабинете: откройте Ещё → Разработчикам → HTTP-уведомления (открыть сразу), укажите URL для уведомлений и отметьте события.
Требования к URL — протокол HTTPS, доменное имя, TCP-порт 443 или 8443, сертификат от общепризнанного центра сертификации, TLS 1.2 или выше.
URL запоминается в платеже при создании: уведомления о платеже и его возвратах приходят на URL, который был указан, когда платёж создавали.
Использование
Когда происходит событие, на которое вы подписались, на URL приходит уведомление. Параметры тела и заголовки — в справочнике: Объект уведомления. В object — данные объекта на момент события.
Ответ и повторы
Подтвердите получение: ответьте кодом HTTP 200 не позже чем через 10 секунд. Тело и заголовки ответа AvantPay не учитывает.
Если ответ другой или его нет, AvantPay повторяет уведомление примерно через 1, 5, 15 и 30 минут, через 1, 2, 4, 8 и 16 часов и в последний раз через 24 часа после первой попытки. Всего 11 попыток.
Проверка подлинности
Когда получите уведомление, проверьте его подлинность: по статусу объекта, подписи или IP-адресу.
Статус объекта
Запросите объект с вашими ключами — GET /v3/payments/{payment_id} или GET /v3/refunds/{refund_id} — и убедитесь, что статус из уведомления актуален.
Подпись
В каждом уведомлении есть заголовки:
Signature-Timestamp— время отправки, Unix-время в секундах.Signature— подпись:v1=и HMAC-SHA256 в шестнадцатеричном виде. Подписывается строка<Signature-Timestamp>.<тело запроса>: время, точка и тело запроса как оно пришло (байты, без разбора и повторной сборки JSON). Ключ HMAC — ваш секретный ключ API.
Как проверить:
- Склейте значение
Signature-Timestamp, точку и тело запроса. - Посчитайте HMAC-SHA256 этой строки секретным ключом и добавьте впереди
v1=. - Сравните с
Signatureфункцией сравнения за постоянное время. - Отклоните уведомление, если
Signature-Timestampотличается от текущего времени больше чем на 5 минут (300 секунд). Так чужой не сможет прислать вам старое уведомление повторно.
Подписывается текущим секретным ключом магазина. После перевыпуска ключа — новым ключом, в том числе в повторах старых уведомлений. Если ключа у магазина нет, заголовков подписи нет.
Функция проверки:
- Python
- Node.js
- PHP
import hashlib
import hmac
import time
from typing import Optional
TOLERANCE_SECONDS = 300
def verify_signature(raw_body: bytes, signature: Optional[str], timestamp: Optional[str], secret_key: str) -> bool:
if not signature or not timestamp or not (timestamp.isascii() and timestamp.isdigit()):
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
signed = timestamp.encode() + b"." + raw_body
expected = "v1=" + hmac.new(secret_key.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature.encode(), expected.encode())
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300;
function verifySignature(rawBody, signature, timestamp, secretKey) {
if (typeof signature !== "string" || typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) {
return false;
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected =
"v1=" +
crypto
.createHmac("sha256", secretKey)
.update(`${ timestamp}.`)
.update(rawBody)
.digest("hex");
return (
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
<?php
function verifySignature(string $rawBody, ?string $signature, ?string $timestamp, string $secretKey): bool
{
if ($signature === null || $timestamp === null || !ctype_digit($timestamp)) {
return false;
}
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secretKey);
return hash_equals($expected, $signature);
}
Сравнивайте подпись функцией сравнения за постоянное время, как в примерах: hmac.compare_digest, timingSafeEqual, hash_equals.
Проверьте свою функцию на этом примере. Тело запроса:
{"type":"notification","event":"payment.succeeded","object":{"id":"2f9b6c1e-4d7a-4c3b-9e8f-1a2b3c4d5e6f","status":"succeeded","paid":true,"amount":{"value":"1000.00","currency":"RUB"}}}
Signature-Timestamp равен 1792324800. С ключом test_secret_key подпись равна v1=07181d18b6de6cfe0c882fd02f7e40c8c0e735f19dc67062c39f57c8d75c995b. Время в примере старое, поэтому проверка времени отклонит его: для самопроверки временно подставьте вместо текущего времени 1792324800.
IP-адреса AvantPay
AvantPay отправляет уведомления только с этих IP-адресов:
79.141.79.16731.129.44.152