Имя каждого события приходит в заголовке 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; statuspaid. По 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. statusexpired; paid_token, paid_at, tx_hashnull. Используйте, чтобы закрыть заказ или предложить покупателю новый счёт.

invoice_cancelled. statuscancelled; заполнены 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_lowProcessing credit заканчивается
billing.processing_credit_depletedProcessing 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 — список может расширяться.

Следующий шаг

Первые 50 подтверждённых оплат — за наш счёт.

Получить $5 на старт ↗