⬡

CRM System

Авторизация для доступа к панели управления

Неверный логин или пароль
⬡
CRMLead Management
Навигация
Лиды
Потоки
Постбеки
Инбек
Лог постбеков
Пользователи
Настройка CRM
API
A
admin

Лиды

Загрузка…

Потоки

Загрузка…

Постбеки

Загрузка…

Инбек — тянет статусы по расписанию

Загрузка…

Лог постбеков

Загрузка…

Пользователи

Загрузка…

Настройка CRM

Загрузка…

API — документация

Загрузка…
Лиды
Потоки
Постбеки
Логи

Лид

Новый поток

Постбеки

Новый пользователь

Пользователь будет видеть лиды только из выбранных потоков.
Потоки не выбраны
Снятые поля пользователь увидит в списке, но их значение будет скрыто (•••). По умолчанию видны все.
Все поля видны

Новый постбек

Настройки отправки постбека
При срабатывании через поток всегда действуют настройки потока. Эти настройки применяются, когда лиды отправляются напрямую на этот постбек (из «Лиды» → «Отправить на постбек»).
Можно через полночь: напр. 16:00–04:00.

Новый инбек

Привязка и опрос
секунд (мин. 30)
Потоки не выбраны
Можно через полночь: напр. 16:00–04:00.

Документация — серверный постбек (JS)

Постбек — это JS-код, который выполняется на сервере, когда лид получает нужный статус. Отсюда можно отправить данные куда угодно по HTTP: в партнёрки (S2S-постбек), рекламные кабинеты (Facebook CAPI, Google Ads и др.), Google Таблицы, CRM, Telegram-бота, свой API. Песочница, таймаут 15 секунд, без доступа к файловой системе.

Тем же кодом можно обработать ответ и по нему сменить статус лида или записать любые его поля — например, поставить deposit и сохранить payout, если партнёрка ответила «одобрено» (см. раздел «Обработка ответа» ниже).

Функции

await http(url, opts)HTTP-запрос. opts = { method, headers, body } (по умолчанию GET). Возвращает { status, body, headers }. Всегда через await.
log(obj)записать объект в «Лог постбеков» (что отправили / какой ответ).
sha256(str)SHA-256 хеш (нужен для FB CAPI: email/телефон хешируются). Строка нормализуется (trim + lowercase).
console.log(...)тоже попадает в лог отправок.
setStatus(key)сменить статус лида. key — ключ статуса из «Настройка CRM». Неизвестный ключ игнорируется (пишется в лог).
setField(key, value)записать любое поле лида (встроенное или кастомное) по его ключу.
setFields({…})записать несколько полей разом; ключ status внутри тоже сменит статус.
statuses / fieldsмассивы текущих статусов {key,label,color} и полей {key,label,type} из настроек — для проверки/выбора в коде.
Ещё доступны: JSON, Date, Math, Buffer, encodeURIComponent, URLSearchParams, Promise.

Статусы лида — из «Настройка CRM»

Текущий статус — lead.status, сменить — setStatus('ключ'). Список ниже тянется из настроек и всегда актуален:

Поля лида — lead

Список полей тянется из «Настройка CRM». Обращение по ключу: lead.ключ; записать — setField('ключ', значение). У переименованного встроенного поля работает и старый ключ (имя колонки).

Всегда доступны (системные, вне настроек): lead.id, lead.stream_id, lead.stream_name, lead.sale_amount.
Дата человеком: new Date(lead.created_at * 1000).toISOString()

⚡ Асинхронность и скорость

  • Сетевые вызовы — асинхронные. Всегда пишите await http(...), чтобы дождаться ответа и записать его в лог.
  • Несколько отправок — параллельно через Promise.all (например, в партнёрку и в Google Таблицу одновременно). Это в разы быстрее, чем по очереди:
    // ❌ медленно — по очереди (сумма задержек)
    await http(url1);
    await http(url2);
    
    // ✅ быстро — параллельно (время самого долгого)
    const [r1, r2] = await Promise.all([
      http(url1),
      http(url2),
    ]);
    log({ pp: r1.status, sheet: r2.status });
  • Не блокируйте поток. Никаких тяжёлых циклов/синхронных вычислений — песочница оборвёт скрипт по таймауту, и весь сервер при этом не должен ждать.
  • Ошибка в коде (или таймаут) попадёт в лог как error, отправка не сорвёт обработку других лидов.

Обработка ответа — менять статус и поля лида

  • После await http(...) посмотрите на res.status (HTTP-код) или разберите res.body и вызовите setStatus(...) / setField(...).
  • Работает с вашим конфигом: статус проверяется по списку из «Настройка CRM» (кастомные — тоже), поле пишется по своему ключу — встроенное в свою колонку, кастомное в хранилище кастомных полей.
  • Сумма (sale_amount). Пишется только если статус помечен «принимает сумму» в «Настройка CRM» (по умолчанию — sale). Пример: setStatus('sale'); setField('sale_amount', d.amount); или setFields({ status: 'sale', sale_amount: d.amount }). Число; нечисло/пусто → null. Перевод на статус без флага «принимает сумму» обнуляет sale_amount.
  • Изменения применяются только при реальной отправке (поток / «Отправить сейчас» / очередь). Кнопка «Тест» и «Проверить код» данные лида не трогают — показывают, что было бы применено.
  • Защищены от записи: id, stream_id, created_at, updated_at. Смена статуса здесь не запускает постбеки заново (защита от циклов).
const res = await http('https://partner.example/api/lead', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ phone: lead.phone, click: lead.sub1 }),
});
log({ http: res.status, response: res.body });

// вариант А — по HTTP-коду ответа
if (res.status === 200)      setStatus('valid');
else if (res.status === 409) setStatus('duplicate');
else                         setStatus('invalid');

// вариант Б — по телу ответа + запись полей
let data = {}; try { data = JSON.parse(res.body); } catch {}
if (data.status === 'approved') {
  setStatus('deposit');                 // ваш статус из «Настройка CRM»
  setFields({ payout: data.amount, subid: data.click_id }); // кастомное + встроенное поле
}

Пример 1. S2S-постбек в партнёрку (GET)

// click_id обычно лежит в sub1; payout — из суммы продажи
const url = 'https://partner.example.com/postback?clickid='
  + encodeURIComponent(lead.sub1)
  + '&status=' + lead.status
  + '&payout=' + (lead.sale_amount || 0);
const r = await http(url);
log({ http: r.status, headers: r.headers, response: r.body });

Пример 2. POST JSON в API партнёрки / своей CRM

const r = await http('https://api.example.com/leads', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ВАШ_ТОКЕН' },
  body: JSON.stringify({
    external_id: lead.id,
    name: lead.name, phone: lead.phone, email: lead.email,
    status: lead.status, amount: lead.sale_amount,
    source: lead.utm_source, sub1: lead.sub1,
  }),
});
log({ http: r.status, headers: r.headers, response: r.body });

Пример 3. Запись строки в Google Таблицу

Сначала в таблице создайте Apps Script (Расширения → Apps Script), вставьте код и опубликуйте как веб-приложение (Развернуть → Веб-приложение, доступ «Все»). Полученный URL /exec вставьте в постбек.

// код Apps Script (в самой Google Таблице):
function doPost(e) {
  const d = JSON.parse(e.postData.contents);
  SpreadsheetApp.getActiveSheet().appendRow([
    new Date(), d.id, d.name, d.phone, d.email, d.status, d.amount
  ]);
  return ContentService.createTextOutput('ok');
}
// код постбека:
const r = await http('https://script.google.com/macros/s/ВАШ_ID/exec', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    id: lead.id, name: lead.name, phone: lead.phone,
    email: lead.email, status: lead.status, amount: lead.sale_amount,
  }),
});
log({ http: r.status }); // 302 — это нормально, строка добавляется

Пример 4. Facebook CAPI (событие Lead)

const PIXEL = lead.sub1;            // пиксель из саба лида
const TOKEN = lead.sub2;            // токен из саба лида
const phone = lead.phone ? String(lead.phone).replace(/[^\d+]/g,'') : '';
const event = {
  action_source: 'website',
  event_name: 'Lead',
  event_time: lead.created_at,
  event_id: 'Lead_' + lead.id,
  user_data: {
    client_ip_address: lead.ip || '',
    client_user_agent: lead.ua || '',
    ...(lead.email ? { em: sha256(lead.email) } : {}),
    ...(phone ? { ph: sha256(phone) } : {}),
    external_id: sha256(String(lead.id)),
  },
};
const r = await http(
  'https://graph.facebook.com/v21.0/' + PIXEL + '/events?access_token=' + TOKEN,
  { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ data:[event] }) }
);
log({ http: r.status, headers: r.headers, response: r.body });

Документация — Инбек (опрос статусов)

Инбек — обратный постбек: по расписанию он тянет статусы из партнёрки и по ответу меняет статус/поля лидов. На каждом тике код получает пачку лидов leads[] (из привязанных потоков, со статусами для опроса) и сам решает — опрашивать по одному или одним bulk-запросом. Песочница, без доступа к файловой системе.

Доступно в коде

leadsмассив лидов пачки (обогащённые: lead.<ключ> для кастомных/переименованных полей).
await http(url, opts)HTTP-запрос. opts = { method, headers, body }. Возвращает { status, body, headers }. Всегда через await.
apply(lead, { status, fields })адресно поменять статус и/или поля конкретного лида из пачки.
setStatus(lead, key)сменить статус лида (то же, что apply(lead,{status:key})).
setField(lead, key, value)записать одно поле лида (встроенное или кастомное).
log(obj), console.log(...)запись в лог прогона инбека.
statuses / fieldsтекущие статусы/поля из «Настройка CRM».
Ещё: sha256, JSON, Date, Math, Buffer, encodeURIComponent, URLSearchParams, Promise.

Статусы — из «Настройка CRM»

Поля лида — lead.<ключ>

Пример 1. Bulk-отчёт (1 запрос на всю пачку — Keitaro-стиль)

const r = await http('https://partner.example/api/conversions?from=today');
const rows = JSON.parse(r.body);
const map = {}; for (const row of rows) map[row.click] = row;   // индексируем по click_id
for (const lead of leads) {
  const row = map[lead.sub1];           // click_id обычно в sub1
  if (!row) continue;                    // ещё нет решения — оставляем как есть
  if (row.status === 'sale')   apply(lead, { status: 'deposit', fields: { payout: String(row.amount) } });
  if (row.status === 'reject') apply(lead, { status: 'trash' });
}
log({ processed: leads.length });

Пример 2. По одному клику (партнёрка отдаёт статус на один клик)

for (const lead of leads) {
  const r = await http('https://partner.example/api/status?click=' + encodeURIComponent(lead.sub1));
  const d = JSON.parse(r.body);
  if (d.status === 'approved') apply(lead, { status: 'deposit', fields: { payout: String(d.amount) } });
  else if (d.status === 'rejected') apply(lead, { status: 'invalid' });
  // 'pending' — ничего не делаем, опросим на следующем тике
}
  • Опрашиваются лиды из привязанных потоков со статусами опроса. Как только лид получает статус вне этого набора — он перестаёт опрашиваться.
  • Сумма (sale_amount): пишется только для статусов с флагом «принимает сумму» (по умолчанию sale): apply(lead, { status: 'sale', fields: { sale_amount: row.amount } }). Число; нечисло/пусто → null.
  • «Тест» делает реальные запросы, но не пишет в лиды — показывает, что изменилось бы. «Запустить сейчас» и планировщик — применяют.
  • Защищены от записи: id, stream_id, created_at, updated_at. Смена статуса инбеком не запускает постбеки повторно.
  • Размер пачки ограничивает число лидов за тик; лиды обходятся по кругу по id (индексная ротация — быстро даже на сотнях тысяч лидов).

Выбор потоков