takemypay

Webhook

Webhook — основной и единственный надёжный канал, по которому вы узнаёте об оплате. Возврат плательщика на return_url подтверждением не является: вкладку могут закрыть, а деньги при этом пройдут.

При каждой смене статуса мы отправляем POST на адрес webhook того проекта, которому принадлежит платёж. Адрес задаётся на странице проекта.

Заголовки

ЗаголовокЗначение
X-SignatureHMAC-SHA256 от сырого тела запроса, в hex.
X-Key-Prefix Первые 10 символов ключа, которым подписано. Нужен при ротации — см. ниже.
X-Payment-IdИдентификатор платежа.
X-Webhook-VersionВерсия формата, сейчас 2.

Тело

application/json
{
  "id": "uuid",
  "external_order_id": "order-123",
  "amount": "1000.00",
  "amount_net": "820.00",
  "fee": "180.00",
  "status": "succeeded",
  "is_test": false,
  "paid_at": "2026-05-19T12:01:32Z",
  "timestamp": "2026-05-19T12:01:32.512Z",
  "nonce": "8f3a..."
}
  • amount — сколько списано с плательщика (gross), amount_net — сколько получаете вы (gross минус fee). В режиме «комиссию платит плательщик» сверяйте заказ по amount_net: именно оно равно сумме, переданной в создание платежа.
  • is_testtrue для платежей, сделанных до активации аккаунта. Такой webhook нужно принять и проверить, но ни в коем случае не отгружать по нему заказ.
  • timestamp и nonce — защита от повторной отправки, см. ниже.

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

Подпись считается так:

X-Signature = HMAC_SHA256(key = hex(SHA256(api_key)), msg = raw_body) → hex

То есть секретом HMAC служит не сам ключ, а его SHA-256-хэш в hex. Так сделано потому, что на нашей стороне хранится только этот хэш — сам ключ мы не знаем и не можем узнать.

🔴 Считайте подпись по сырому телу запроса, до разбора JSON. Если сначала распарсить и потом сериализовать обратно, порядок ключей и пробелы поменяются, и подпись не сойдётся — это самая частая ошибка интеграции.

Подписывается новейший активный ключ проекта. Поэтому при ротации, пока у проекта есть несколько активных ключей, смотрите на X-Key-Prefix — он говорит, против какого из них проверять.

Проверка в Node.js

javascript
import crypto from "node:crypto";

function verifyWebhook(rawBody, signature, apiKey) {
  const signingSecret = crypto.createHash("sha256").update(apiKey).digest("hex");
  const expected = crypto.createHmac("sha256", signingSecret)
    .update(rawBody)
    .digest("hex");
  if (expected !== signature) return false;

  // Replay defense: reject bodies older than 5 minutes.
  const body = JSON.parse(rawBody);
  const ts = Date.parse(body.timestamp);
  if (Date.now() - ts > 5 * 60 * 1000) return false;

  return true;
}

Проверка в Python

python
import hmac, hashlib, json
from datetime import datetime, timezone

def verify_webhook(raw_body: bytes, signature: str, api_key: str) -> bool:
    signing_secret = hashlib.sha256(api_key.encode()).hexdigest()
    expected = hmac.new(signing_secret.encode(), raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        return False

    body = json.loads(raw_body)
    ts = datetime.fromisoformat(body["timestamp"])
    age = (datetime.now(timezone.utc) - ts).total_seconds()
    return age <= 300  # reject if > 5 minutes old

Защита от повтора

Внутри подписанного тела лежат timestamp (ISO 8601) и nonce. Отвергайте тело старше 5 минут — иначе перехваченный однажды валидный запрос можно будет проигрывать сколько угодно раз, и подпись у него будет правильная.

Что считается доставкой

Доставкой считается ответ 2xx. Всё остальное — повод повторить, включая редиректы: 301 и 302 для нас не успех, мы по ним не переходим.

Проверьте это отдельно. Эндпоинт, отвечающий редиректом (типичный случай — фреймворк уводит с /webhook на /webhook/ или на https), выглядит рабочим в браузере и при этом не принимает ни одного уведомления. Обнаруживается это обычно по недоотгруженным заказам, а не по ошибке.

Расписание повторов — шесть попыток, пока не придёт 2xx:

ПопыткаЧерез
1сразу
230 секунд
35 минут
430 минут
52 часа
612 часов

После шестой неудачной попытки доставка прекращается. Восстановить состояние можно запросом GET /api/v1/payments/{id}.

Обработчик должен быть идемпотентным

Один и тот же платёж может прийти к вам несколько раз: повтор после сетевого таймаута, когда ваш ответ не дошёл до нас, — штатная ситуация. Ориентируйтесь на id платежа и на то, что заказ уже обработан, а не на сам факт получения запроса.