Имя каждого события приходит в заголовке GramPay-Event, но JSON-тело сейчас имеет две формы: invoice-события используют общую обёртку, а billing-события — плоский payload.
Общая обёртка
{
"update_id": 1024,
"update_type": "invoice_paid",
"request_date": "2026-07-17T09:12:33Z",
"payload": { … }
}
update_id— числовой ID события;update_type— имя события, дублируется в заголовкеGramPay-Event;request_date— время формирования, ISO 8601;payload— данные события.
Invoice-события
Три события описывают жизненный цикл счёта:
| Событие | Когда приходит |
|---|---|
invoice_paid | Оплата найдена в блокчейне и подтверждена |
invoice_expired | Истёк срок действия счёта (expires_in) |
invoice_cancelled | Счёт отменён продавцом (в боте, кабинете или через API) |
payload во всех трёх — это объект invoice, тот же, что возвращает API. Отличия по событиям — в заполненности полей:
invoice_paid. Заполнены paid_token (usdt/usdc), paid_at, tx_hash; status — paid. По tx_hash можно построить ссылку на блокчейн-эксплорер.
{
"update_id": 1024,
"update_type": "invoice_paid",
"request_date": "2026-07-17T09:12:33Z",
"payload": {
"public_id": "GU91XHr8AUNQ8l9r",
"hash": "GU91XHr8AUNQ8l9r",
"status": "paid",
"amount_usd": "180.00",
"amount_usdt": "180.013427",
"amount_usdc": null,
"paid_token": "usdt",
"paid_source": "onchain",
"public_description": "Заказ #481",
"internal_note": "Клиент A",
"payload": "order_123",
"web_app_invoice_url": "https://app.grampaybot.com/invoices/GU91XHr8AUNQ8l9r",
"bot_invoice_url": "https://t.me/GramPayBot?start=pay_GU91XHr8AUNQ8l9r",
"tx_hash": "8f21…d491",
"created_at": "2026-07-17T08:55:01Z",
"expiration_date": "2026-07-17T09:55:01Z",
"paid_at": "2026-07-17T09:12:30Z",
"cancelled_at": null,
"cancel_reason": null
}
}
invoice_expired. status — expired; paid_token, paid_at, tx_hash — null. Используйте, чтобы закрыть заказ или предложить покупателю новый счёт.
invoice_cancelled. status — cancelled; заполнены cancelled_at и, если указана, cancel_reason.
Связывайте событие со своим заказом через поле payload (ваш ID заказа, переданный при создании) — а не через суммы или время.
Billing-события
Billing-события приходят на тот же endpoint с префиксом billing., но не используют поля update_id, update_type, request_date и вложенный payload. Phoenix добавляет к метаданным события поля верхнего уровня type и account_id:
{
"type": "billing.account_unserviceable",
"account_id": "6d86f4e0-6ad2-4fb9-9f0c-f2eb9ab5c4c7",
"reason": "processing_credit_depleted"
}
Точный набор остальных полей зависит от события:
| Событие | Что означает |
|---|---|
billing.balance_topup_applied | Пополнение баланса зачислено |
billing.package_activated | Пакет платежей активирован |
billing.package_queued | Пакет куплен и поставлен в очередь |
billing.current_package_expiring_soon | Текущий пакет скоро истечёт |
billing.current_package_expired | Текущий пакет истёк |
billing.next_package_activated | Активирован следующий пакет из очереди |
billing.current_package_low_payments | В пакете заканчиваются оплаты |
billing.processing_credit_low | Processing credit заканчивается |
billing.processing_credit_depleted | Processing credit исчерпан |
billing.account_unserviceable | Аккаунт не обслуживается: создание счетов вернёт 402 |
billing.account_service_restored | Обслуживание восстановлено |
Минимум, который стоит обрабатывать: billing.account_unserviceable и billing.account_service_restored — они напрямую влияют на работу createInvoice. Остальные полезны для алертов, чтобы пополнить баланс до остановки.
Тестовое событие
webhook_test приходит при настройке URL и по кнопке «Отправить тест» — состав описан в разделе webhooks. Обработчик должен отвечать на него 2xx, но не выполнять бизнес-логику.
Практика обработки
app.post('/webhooks/grampay', (req, res) => {
if (!verifySignature(req.rawBody, req.headers['grampay-signature'], SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.rawBody);
const eventType = req.headers['grampay-event'];
switch (eventType) {
case 'webhook_test':
break; // просто 200
case 'invoice_paid':
markOrderPaid(event.payload.payload, event.payload.tx_hash); // идемпотентно!
break;
case 'invoice_expired':
case 'invoice_cancelled':
closeOrder(event.payload.payload, eventType);
break;
default:
if (eventType.startsWith('billing.')) notifyOps(event);
}
res.status(200).end();
});
Неизвестные типы событий логируйте и подтверждайте 2xx — список может расширяться.