Skip to content

Callbacks (webhooks)

Мы шлём POST-запросы на ваш callback_url, когда у лида меняется статус. Это основной канал доставки событий — не polling.

Конфигурация

При создании партнёра менеджер выставляет:

ПолеОписание
callback_urlHTTPS URL вашего endpoint'а, куда мы будем стучаться.
callback_secretСекрет для HMAC-подписи payload (вы получаете при создании, один раз).
callback_events(опц.) whitelist событий. Пустой = слать всё.
status_mapping(опц.) перевод наших кодов в ваши.

Сменить callback_url или callback_events можно в любой момент — попросите менеджера.

Формат запроса

http
POST https://your-domain.com/cartelcrm/callback HTTP/1.1
X-Signature: sha256=<HMAC-SHA256(raw_body, callback_secret)>
X-Timestamp: 1748390123
X-Event: lead.ftd
Content-Type: application/json
User-Agent: CartelCRM-Webhook/1.0

{
  "event": "lead.ftd",
  "external_lead_id": "aff42-lead-001",
  "internal_id": "le-12345",
  "status": "won",
  "our_status": "ftd",
  "is_depositor": true,
  "timestamp": "2026-05-27T14:22:03+00:00"
}

Заголовки

HeaderОписание
X-Signaturesha256=<hex(HMAC-SHA256(raw_body, callback_secret))> — проверить!
X-Timestampunix-секунды, момент отправки. Дрифт >5 минут — отказ.
X-EventИмя события (дублирует поле event в payload).

Поля payload

ПолеТипОписание
eventstringТип события (см. ниже).
external_lead_idstringВаш id, который вы прислали при POST /v1/inbound/leads.
internal_idstringНаш internal id (le-{numeric}).
statusstringСтатус в ваших терминах (если есть status_mapping).
our_statusstringНаш канонический код. Полезно для отладки.
is_depositorbooleantrue если лид уже стал клиентом (FTD произошёл).
timestampstringISO-8601, момент события.

Никаких денежных полей

Поля amount, currency, ftd_amount, rtd_amount в callback никогда не передаются. Это договорное ограничение. Финансовая часть — только на нашей стороне.

События

Полный список — см. События. Кратко:

  • lead.accepted — после успешного inbound.
  • lead.rejected — если intake отклонён бизнес-правилами.
  • lead.status_changed — любая смена статуса (кроме перехода в FTD/RTD).
  • lead.ftd — зафиксирован первый депозит, лид стал клиентом.
  • lead.rtd — повторный депозит.

Что вы должны вернуть

УсловиеПоведение
HTTP 2xxСчитаем доставленным, ретраев больше не будет.
HTTP 4xx (не 408, не 429)Считаем «не примете никогда» — даём только ещё 1 ретрай.
HTTP 5xx, 408, 429, timeoutРетраим по экспоненте.
Нет ответа за 10 секундТаймаут, считается неуспехом.

Возвращайте быстро

Не делайте тяжёлой работы синхронно в callback handler'е. Положите запрос в свою очередь и сразу верните 200. У вас есть 10 секунд.

Retry-политика

При ошибке мы ретраим до 5 попыток с экспоненциальной задержкой:

ПопыткаЗадержка
1сразу
2+1 минута
3+5 минут
4+30 минут
5+2 часа

После 5-й неудачи событие помечается как dead и больше не ретраится. Если вам нужно восстановить такой webhook — напишите менеджеру.

Логи и мониторинг

В админке Cartel CRM есть страница «Outgoing webhooks» — там видны:

  • очередь, доставленные, мёртвые
  • последний HTTP-код и тело ответа
  • попыток сделано / осталось
  • кнопка «retry» (для менеджера)

Если вам нужен временный доступ — напишите менеджеру.

Идемпотентность на вашей стороне

Один и тот же (internal_id, event) может прилететь дважды (если первая попытка не дошла, но успела долететь). Сохраняйте локально seen(internal_id + event + timestamp) и игнорируйте повторы. Минимальная защита.

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

js
import crypto from 'node:crypto'

app.post('/cartelcrm/callback', (req, res) => {
  const rawBody = req.rawBody  // важно: именно сырое тело, а не JSON.parse(req.body)
  const sig     = req.headers['x-signature'] ?? ''
  const ts      = Number(req.headers['x-timestamp'] ?? 0)

  // 1. Timestamp window
  if (Math.abs(Date.now() / 1000 - ts) > 300) {
    return res.status(401).send('timestamp_out_of_range')
  }

  // 2. Signature
  const cleanSig = sig.startsWith('sha256=') ? sig.slice(7) : sig
  const expected = crypto
    .createHmac('sha256', process.env.CALLBACK_SECRET)
    .update(rawBody)
    .digest('hex')

  if (
    cleanSig.length !== expected.length ||
    !crypto.timingSafeEqual(
      Buffer.from(cleanSig, 'hex'),
      Buffer.from(expected, 'hex'),
    )
  ) {
    return res.status(403).send('bad_signature')
  }

  // 3. Bytes verified — обрабатываем
  const event = JSON.parse(rawBody)
  queue.push(event)
  res.status(200).send('ok')
})

См. HMAC подпись для PHP / Python / Go.

Тест callback'а

Менеджер может «выстрелить» тестовый callback из админки CRM: POST /api/affiliates/{id}/test-callback — мы пришлём вам синтетический lead.accepted с фиктивным internal_id. Удобно проверить, что ваш endpoint вообще доступен и проверяет подпись.

Закрытая партнёрская документация. Не для публичного распространения.