> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vargopartners.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Вебхуки

> События об оплате, истёкшем счёте и окончании подписки на ваш сервер

Вебхук — `POST` на адрес вашего сервера, когда с вашим клиентом что-то произошло. Он избавляет от опроса: оплату вы видите сразу, а клиенту, у которого кончается срок, можете напомнить о продлении сами. Напоминания платформы получают только клиенты с Telegram, клиентам вашего бота или сайта напоминаете вы.

Источник истины по-прежнему запрос `GET`. Вебхук может прийти дважды, опоздать или прийти не по порядку. Перед действием по событию перечитайте счёт или клиента.

## Подключение

<Steps>
  <Step title="Адрес">
    В кабинете партнёра откройте раздел [«API»](https://admin.vargopartners.com/reseller/api) и добавьте адрес в блоке «Адреса вебхуков». Подходит только `https://` на порту 443 с публичным адресом сервера. Адресов может быть до 10, для каждого выбираются свои события.
  </Step>

  <Step title="Секрет">
    Скопируйте секрет адреса (`whsec_…`) и сохраните его на сервере. Им проверяется подпись, а посмотреть его снова можно кнопкой «Секрет».
  </Step>

  <Step title="Тест">
    Нажмите «Отправить тест». Придёт событие `webhook.test` с пустым `data`, а кабинет покажет, дошло ли оно: код ответа вашего сервера и время ответа. Тест — не чаще 5 раз в минуту на адрес.
  </Step>
</Steps>

Событие приходит только на адреса, которые были подписаны на его тип в момент события. Адрес, добавленный позже, старых событий не получает.

## Запрос

```http theme={"dark"}
POST /vargo/webhook HTTP/1.1
content-type: application/json
webhook-id: msg_7f3c2a9e0b1d4c5e8f6a7b8c9d0e1f2a
webhook-timestamp: 1790770020
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

{"data":{…},"timestamp":"2026-09-30T12:07:00.412305Z","type":"payment.succeeded"}
```

* `webhook-id` — номер события. Он одинаков во всех повторах одного события: по нему отсекайте дубли.
* `webhook-timestamp` — время отправки этой попытки, Unix-секунды.
* `webhook-signature` — подпись. Во время перевыпуска секрета подписей две через пробел: новым секретом и прежним.
* Тело: `type` — тип события, `timestamp` — когда событие произошло (ISO 8601, UTC, с долями секунды), `data` — объект события. Тело одинаково во всех повторах.

## Проверка подписи

Каждое событие подписано секретом адреса. По подписи ваш сервер понимает, что запрос пришёл от VARGO и по дороге не изменился: адрес публичный, отправить на него запрос может кто угодно. Подпись сделана по открытому стандарту [Standard Webhooks](https://www.standardwebhooks.com), поэтому проверить её можно готовой библиотекой, без своего кода.

<CodeGroup>
  ```python Python theme={"dark"}
  # pip install standardwebhooks
  from fastapi import FastAPI, HTTPException, Request
  from standardwebhooks.webhooks import Webhook, WebhookVerificationError

  app = FastAPI()
  webhook = Webhook("whsec_…")  # секрет адреса из кабинета


  @app.post("/vargo/webhook")
  async def vargo_webhook(request: Request):
      try:
          event = webhook.verify(await request.body(), dict(request.headers))
      except WebhookVerificationError:
          raise HTTPException(status_code=400)
      # event["type"], event["data"]; дубли — по request.headers["webhook-id"]
      return {"ok": True}
  ```

  ```javascript Node.js theme={"dark"}
  // npm install standardwebhooks express
  import express from "express";
  import { Webhook } from "standardwebhooks";

  const app = express();
  const webhook = new Webhook("whsec_…"); // секрет адреса из кабинета

  app.post("/vargo/webhook", express.raw({ type: "application/json" }), (req, res) => {
    let event;
    try {
      event = webhook.verify(req.body, req.headers);
    } catch {
      return res.sendStatus(400);
    }
    // event.type, event.data; дубли — по req.headers["webhook-id"]
    res.sendStatus(200);
  });
  ```
</CodeGroup>

* **Тело — как пришло.** Подпись считается от тела байт в байт, поэтому передавайте в проверку сырое тело до разбора JSON: `await request.body()` в FastAPI, `express.raw` в Express. Разобранный и собранный заново JSON проверку не пройдёт.
* **Часы сервера — точные.** Запрос, отправленный больше 5 минут назад, библиотека отклоняет: так не пройдёт повтор перехваченного запроса. Если часы сервера отстают, отклоняться будут и настоящие события.

Библиотеки стандарта есть и для Go, PHP, Ruby, Java, C#, Rust — список на [standardwebhooks.com](https://www.standardwebhooks.com).

<Accordion title="Проверка без библиотеки">
  1. Соберите строку `{webhook-id}.{webhook-timestamp}.{тело}`, тело — как пришло.
  2. Ключ — секрет без префикса `whsec_`, декодированный из base64.
  3. Посчитайте HMAC-SHA256 строки на этом ключе и закодируйте результат в base64.
  4. В `webhook-signature` через пробел лежат подписи вида `v1,<base64>`. Событие подлинное, если ваша подпись совпала хотя бы с одной из них. Сравнивайте функцией постоянного времени: `hmac.compare_digest` в Python, `crypto.timingSafeEqual` в Node.js.
  5. Отклоните запрос, если `webhook-timestamp` отличается от текущего времени больше чем на 5 минут.
</Accordion>

## Ответ и повторы

Событие считается доставленным, когда ваш сервер ответил кодом `2xx` не дольше чем за 15 секунд. Тело ответа не важно. Отвечайте сразу, а долгую работу делайте после ответа.

Любой другой исход — код не `2xx`, таймаут, обрыв соединения, редирект — это сбой, и событие приходит снова. Всего 10 попыток. Повторы идут через 5 секунд, 5 минут, 30 минут, 2, 5, 10, 14, 20 и 24 часа, с разбросом ±10%, так что последняя попытка приходит примерно через трое суток. Если в ответе с ошибкой есть заголовок `Retry-After`, следующая попытка придёт не раньше этой паузы; пауза длиннее 24 часов считается за 24 часа. Раньше срока по расписанию попытка не придёт.

Адрес отключается сам, если он пять суток подряд не принял ни одного события или однажды ответил `410 Gone`. Тогда вам придёт сообщение в бот [@vargo\_robot](https://t.me/vargo_robot?start=docs_help). Включить адрес снова можно в кабинете. После починки сервера события, которые до него не дошли, отправляются заново пунктом «Повторить неудачные» в меню адреса: за последние сутки, 3, 7 или 30 дней.

## Секрет

Секрет адреса можно перевыпустить в кабинете. Следующие 24 часа события подписываются двумя секретами, новым и прежним, поэтому сервер, который ещё не получил новый секрет, продолжает их принимать. Замените секрет на сервере в течение этих суток.

## События

Новые типы событий появляются без смены версии. Адрес, подписанный на все события, получит и их: незнакомый тип отвечайте `2xx` и пропускайте.

### payment.succeeded

Счёт оплачен, доступ клиенту выдан. `data` — счёт в том же виде, что в ответе [`GET /payments/{payment_id}`](/api/get-payment): `status: succeeded` и клиент со ссылкой подписки в `client`.

```json theme={"dark"}
{
  "type": "payment.succeeded",
  "timestamp": "2026-09-30T12:07:00.412305Z",
  "data": {
    "payment_id": "5b0c9d0e-7c1a-4f3e-9b8e-2a6d1c0f4e21",
    "status": "succeeded",
    "purpose": "subscription",
    "client_id": "user-42",
    "plan_id": 3,
    "devices": null,
    "method": "sbp",
    "amount": "150.00",
    "platform_amount": "100.00",
    "partner_share": "50.00",
    "currency": "RUB",
    "payment_url": null,
    "created_at": "2026-09-30T12:01:00Z",
    "expires_at": "2026-09-30T12:31:00Z",
    "paid_at": "2026-09-30T12:07:00Z",
    "client": {
      "client_id": "user-42",
      "created_at": "2026-09-25T12:00:00Z",
      "subscription": {
        "status": "active",
        "is_trial": false,
        "expires_at": "2026-10-30T12:07:00Z",
        "days_left": 30,
        "subscription_url": "https://sub.example.com/AbCdEf12",
        "connect": [ … ],
        "devices_limit": 3
      },
      "trial_available": false
    }
  }
}
```

### payment.expired

Счёт истёк неоплаченным: клиент не заплатил за 30 минут. Событие приходит, когда сервис оплаты отменит счёт, и не позже чем через час после истечения. `data` — счёт со `status: expired` и `client: null`.

Истечение не окончательно: если клиент всё же заплатит позже, оплата пройдёт, и следом придёт `payment.succeeded`. По этому событию удобно напомнить клиенту о брошенной оплате.

### subscription.expiring

До конца срока подписки 72 часа или 24 часа. Каждый порог приходит один раз за срок. Клиент, у которого до конца меньше 24 часов в момент первой проверки, получит только порог 24. У пробной подписки порога 72 нет: столько длится весь пробный период. После продления отсчёт начинается заново.

```json theme={"dark"}
{
  "type": "subscription.expiring",
  "timestamp": "2026-10-27T12:10:03.118042Z",
  "data": {
    "threshold_hours": 72,
    "client": { "client_id": "user-42", "subscription": { "status": "active", "expires_at": "2026-10-30T12:07:00Z", … }, … }
  }
}
```

`client` — в том же виде, что ответ [`GET /clients/{client_id}`](/api/get-client).

### subscription.expired

Срок подписки вышел. `data` — `{"client": …}`, `timestamp` — момент окончания срока. Если клиент продлит подписку, после следующего окончания событие придёт снова.

События срока проверяются раз в 5 минут. Заблокированным клиентам они не приходят.
