Меню

Reference

Вебхуки

Recca сообщает о событиях сети на ваш адрес. Каждое тело подписано HMAC-SHA256, в подписи — метка времени.

Включение

Адрес приёма задаётся в кабинете на странице «Вебхуки» организации: только HTTPS, адреса внутренних сетей и localhost не принимаются. Секрет подписи выдаётся там же. Машинным путём CPA-сети адрес задаёт PUT /federation/v1/cpa/self/webhook (право cpa:self:write): секрет приходит в ответе на первый вызов или при rotate_secret, повторный вызов его не возвращает — сохраните сразу.

Конверт

Тело одно на все события:

{
  "event_id": "fwd_01K5...",
  "event_type": "cpa.conversion.approved",
  "event_version": "1",
  "organization_id": "org_01K5...",
  "occurred_at": "2026-10-08T10:00:00.000Z",
  "delivered_at": "2026-10-08T10:00:03.120Z",
  "data": { ... }
}

event_id — идентификатор доставки: он не меняется при повторах, по нему удобно отсекать дубли. occurred_at — когда случилось событие, delivered_at — когда отправлено; у событий cpa.* первое есть, у остальных его нет. Заголовки запроса — X-Recca-Signature, X-Recca-Delivery-Id и X-Recca-Event. Таймаут ответа — 10 секунд.

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

X-Recca-Signature: t=<unix-секунды>,v1=<hex(HMAC-SHA256(secret, "<t>.<сырое тело>"))>

Подписывается строка «метка времени, точка, тело запроса байт в байт» — разбирать JSON до проверки нельзя. Запросы с меткой старше пяти минут отклоняйте: так перехваченную доставку не воспроизвести.

app.post(
  "/api/recca/webhook",
  express.raw({ type: "application/json" }),  // ВАЖНО: сырое тело
  (req, res) => {
    const m = /t=(\d+),v1=([0-9a-f]{64})/i.exec(req.header("X-Recca-Signature") || "")
    if (!m) return res.status(401).end()

    const expected = crypto
      .createHmac("sha256", process.env.RECCA_WEBHOOK_SECRET)
      .update(m[1] + "." + req.body.toString("utf-8"))
      .digest("hex")
    const fresh = Math.abs(Date.now() / 1000 - Number(m[1])) <= 300
    const ok = crypto.timingSafeEqual(Buffer.from(m[2], "hex"), Buffer.from(expected, "hex"))
    if (!fresh || !ok) return res.status(401).end()

    const event = JSON.parse(req.body.toString("utf-8"))
    // разбор по event.event_type; отвечайте 2xx быстро
    res.json({ received: true })
  }
)

Каталог событий

СобытиеКогда приходитПоля data
lead.createdПринята заявка (POST /federation/v1/leads).lead_id, referrer_user_id, deeplink_id, external_id, lead_source, contact, metadata, created_at
bonus.approvedНачисленный бонус подтверждён и стал доступен.bonus_transaction_id, user_id, amount_rub, source_type, source_id, approved_at
conversion.cascadedКонверсия принята (POST /federation/v1/conversions), вознаграждение распределено по цепочке.conversion_id, external_id, event_type, amount_rub, cascade_amount_rub, cascade_legs_count, cascade_legs
conversion.reversedКонверсия отменена или возвращена, начисленное списывается.conversion_id, external_id, status, reversed_amount_rub, fully_reversed, reversed_legs, unreversed_legs, reason
cpa.conversion.approvedCPA-сеть: холд конверсии дозрел, вознаграждение доступно партнёру.external_id, goal, payout_rub, status, partner_external_id
cpa.conversion.declinedCPA-сеть: конверсия отклонена.external_id, goal, payout_rub, status, partner_external_id
cpa.conversion.reversedCPA-сеть: подтверждённая конверсия отменена.external_id, goal, payout_rub, status, partner_external_id
cpa.payout.paidCPA-сеть: оператор отметил выплату выплаченной.payout_id, amount_rub, partner_external_id, paid_at

Суммы — в рублях (amount_rub, payout_rub). Состав событий может пополняться новыми полями: разбирайте тело терпимо к лишним ключам.

Повторы и идемпотентность

Доставка считается успешной при ответе 2xx. Любой другой ответ или таймаут — повтор с паузами 1 минута, 5 минут, 15 минут, 1 час, 6 часов; всего шесть попыток. После шестой неудачи событие уходит в очередь недоставленных, а в журнале аудита организации появляется запись «webhook.delivery_dlq».

Событие может прийти больше одного раза: храните уже обработанные event_id (достаточно 30 суток) и повторные пропускайте.

Подробности по CPA-двери и клиентская проверка подписи в Go — в разделе SDK.