Тема
Callbacks (webhooks)
Мы шлём POST-запросы на ваш callback_url, когда у лида меняется статус. Это основной канал доставки событий — не polling.
Конфигурация
При создании партнёра менеджер выставляет:
| Поле | Описание |
|---|---|
callback_url | HTTPS 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-Signature | sha256=<hex(HMAC-SHA256(raw_body, callback_secret))> — проверить! |
X-Timestamp | unix-секунды, момент отправки. Дрифт >5 минут — отказ. |
X-Event | Имя события (дублирует поле event в payload). |
Поля payload
| Поле | Тип | Описание |
|---|---|---|
event | string | Тип события (см. ниже). |
external_lead_id | string | Ваш id, который вы прислали при POST /v1/inbound/leads. |
internal_id | string | Наш internal id (le-{numeric}). |
status | string | Статус в ваших терминах (если есть status_mapping). |
our_status | string | Наш канонический код. Полезно для отладки. |
is_depositor | boolean | true если лид уже стал клиентом (FTD произошёл). |
timestamp | string | ISO-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 вообще доступен и проверяет подпись.