Skip to content

Сценарии использования ​

Два сквозных сценария от запроса до результата. Базовый адрес везде один: https://platformapi.bigbencrm.ru/api/public/v1. Ключ и скоупы — см. Аутентификация, коды ошибок — Коды ошибок, лимит запросов — Лимиты запросов.

Сценарий 1. Выгрузка учеников ​

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

Шаг 1 — первая полная выгрузка ​

GET /students отдаёт список постранично, максимум 100 записей за раз (per_page, по умолчанию 20). Идём по страницам, пока не наберём весь meta.total.

GET /api/public/v1/students?page=1&per_page=100
Authorization: Bearer bb_a1b2_c3d4e5f6...

Ответ:

json
{
  "data": [
    {
      "id": 88123,
      "fio": "Иванова Мария",
      "phone": "+79261234567",
      "email": "maria@example.com",
      "balance_kopecks": 450000,
      "active_groups": null
    }
  ],
  "meta": { "page": 1, "per_page": 100, "total": 214 }
}

active_groups в списке всегда null — список групп ученика отдаёт только GET /students/{id} (карточка одного ученика). balance_kopecks — баланс целыми копейками, не float.

Шаг 2 — обработка лимита запросов в минуту ​

При превышении лимита (сколько именно запросов в минуту разрешено на ваш ключ — зависит от тарифа школы, см. «Лимиты запросов») API отвечает 429 с заголовком Retry-After (секунды до следующей попытки) и тем же конвертом ошибки:

json
{ "error": { "code": "rate_limit_exceeded", "message": "..." } }

Правильная реакция — подождать Retry-After секунд и повторить именно этот запрос. Пропуск страницы после 429 оставит дыру в выгрузке: тела ответа в нём нет.

Шаг 3 — инкрементальная синхронизация ​

При повторных запусках не нужно выгружать всех учеников заново — updated_since фильтрует по времени последнего изменения записи:

GET /api/public/v1/students?updated_since=2026-08-13T10:00:00%2B00:00&per_page=100

Сохраняйте время запуска синхронизации перед первым запросом страницы и используйте его как updated_since в следующем запуске — так в выборку попадут и записи, изменённые ровно во время текущего прогона.

Первая синхронизация — всегда полная

updated_since фильтрует по полю последнего изменения записи в CRM. У части учеников это поле — legacy-заглушка (запись никогда не редактировалась после переноса из старой системы) и никогда не станет «больше» вашего updated_since, сколько бы времени ни прошло. Такие записи не попадут в инкрементальную выборку ни при каком запуске. Поэтому updated_since — это способ ДОГНАТЬ изменения после того, как у вас уже есть полная копия (шаг 1), а не безопасная замена первой полной выгрузки.

Код: PHP, полная и инкрементальная синхронизация с обработкой 429 ​

php
<?php

declare(strict_types=1);

final class BigBenStudentsSync
{
    private const BASE_URL = 'https://platformapi.bigbencrm.ru/api/public/v1';

    public function __construct(private readonly string $apiKey, private readonly string $stateFile) {}

    /** @return array<int, array<string, mixed>> */
    public function sync(): array
    {
        $runStartedAt = gmdate('Y-m-d\TH:i:s\+00:00');
        $updatedSince = $this->readLastSyncTime();

        $students = [];
        $page = 1;

        do {
            $query = [
                'page' => $page,
                'per_page' => 100,
            ];
            if ($updatedSince !== null) {
                $query['updated_since'] = $updatedSince;
            }

            $response = $this->request('/students?' . http_build_query($query));

            foreach ($response['data'] as $student) {
                $students[] = $student;
            }

            $total = $response['meta']['total'];
            $page++;
        } while (($page - 1) * 100 < $total);

        $this->writeLastSyncTime($runStartedAt);

        return $students;
    }

    private const MAX_RETRY_ATTEMPTS = 5;

    /** @return array{data: array, meta?: array} */
    private function request(string $path): array
    {
        for ($attempt = 1; $attempt <= self::MAX_RETRY_ATTEMPTS; $attempt++) {
            $ch = curl_init(self::BASE_URL . $path);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HEADER => true,
                CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $this->apiKey],
            ]);

            $raw = curl_exec($ch);
            $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
            curl_close($ch);

            $headers = substr($raw, 0, $headerSize);
            $body = substr($raw, $headerSize);

            if ($status === 429 && $attempt < self::MAX_RETRY_ATTEMPTS) {
                sleep($this->retryAfterSeconds($headers));
                continue;
            }

            if ($status !== 200) {
                throw new RuntimeException("BigBen API error: HTTP {$status}: {$body}");
            }

            return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
        }

        throw new RuntimeException("BigBen API error: превышен лимит попыток ({$path})");
    }

    private function retryAfterSeconds(string $headers): int
    {
        if (preg_match('/^Retry-After:\s*(\d+)/mi', $headers, $matches) === 1) {
            return (int) $matches[1];
        }

        return 5; // запасное значение, если заголовок не пришёл
    }

    private function readLastSyncTime(): ?string
    {
        if (!file_exists($this->stateFile)) {
            return null;
        }

        return trim(file_get_contents($this->stateFile)) ?: null;
    }

    private function writeLastSyncTime(string $timestamp): void
    {
        file_put_contents($this->stateFile, $timestamp);
    }
}

$sync = new BigBenStudentsSync(
    apiKey: getenv('BIGBEN_API_KEY'),
    stateFile: __DIR__ . '/.bigben-last-sync',
);

$students = $sync->sync();
echo count($students) . " учеников получено\n";

Сценарий 2. Приём лида с сайта школы ​

Задача: форма на сайте школы («Записаться на пробный урок») отправляет заявку напрямую в BigBen CRM через POST /leads (в интерфейсе CRM это раздел «Заявки», см. также словарь терминов), без промежуточного бэкенда школы, который сам ведёт базу лидов.

Шаг 1 — форма отправляет данные на ваш обработчик ​

Обработчик на сайте школы получает поля формы (имя, телефон, необязательный комментарий) и должен переслать их в BigBen CRM с ключом школы — сам ключ не должен попадать в браузер посетителя.

Шаг 2 — запрос к API с идемпотентностью ​

POST /leads требует API-ключ со скоупом write и обязательный заголовок Idempotency-Key — без него запрос отклоняется с 422 missing_idempotency_key. Генерируйте новый ключ идемпотентности на каждую попытку отправки формы (например, UUID); повторная отправка с тем же ключом в течение 24 часов вернёт тот же результат и не создаст второго лида — это защищает от дублей при повторном клике или ретрае сети на стороне посетителя.

POST /api/public/v1/leads
Authorization: Bearer bb_a1b2_c3d4e5f6...
Idempotency-Key: 6f9c2e1a-8b3d-4e0a-9c7b-1a2b3c4d5e6f
Content-Type: application/json

{
  "name": "Пётр Сидоров",
  "phone": "+79261234567",
  "comment": "Интересует английский для ребёнка 10 лет",
  "source": "site-form"
}

Успешный ответ — 201:

json
{
  "data": {
    "id": 55210,
    "name": "Пётр Сидоров",
    "phone": "+79261234567",
    "source": "site-form",
    "comment": "Интересует английский для ребёнка 10 лет",
    "funnel_id": 0,
    "pipeline_status_id": 118,
    "created_at": "2026-08-14T10:16:00.000000Z"
  }
}

Если source не передать, CRM подставит api. Если не передать funnel_id (или передать 0) — лид попадёт в «Основную воронку» школы, в статус с наименьшим приоритетом (обычно «Входящие»). Указывать конкретную воронку (funnel_id) имеет смысл только если она принадлежит этой же школе — иначе будет 422 funnel_not_found.

funnel_id в ответе — не всегда id реальной строки

funnel_id в ответе — это то же значение, что было в запросе (после проверки владения школой, если оно не нулевое): 0 (или отсутствие поля) даёт в ответе funnel_id: 0, а 0 — служебный сентинел «Основная воронка», не строка в таблице воронок. Не сохраняйте funnel_id из ответа как внешний ключ на воронку без проверки: ненулевое значение действительно указывает на существующую (проверенную) воронку школы, а 0 — нет.

Шаг 3 — обработка ошибок валидации ​

Отсутствие обязательного поля возвращает 422 с деталями по каждому полю:

json
{
  "error": {
    "code": "validation_failed",
    "message": "...",
    "fields": {
      "phone": ["Поле phone обязательно для заполнения."]
    }
  }
}

Показывайте посетителю сайта отдельное сообщение об ошибке для каждого поля из fields, не общий текст «что-то пошло не так» — так понятно, что именно исправить в форме.

Что видит школа в CRM ​

Лид сразу появляется в разделе лидов CRM в указанной воронке (или в «Основной», если funnel_id не передан), в первом статусе по приоритету. Отдельно от этого, если у школы настроена подписка на событие lead.created (см. Вебхуки), тот же лид одновременно уходит вебхуком на URL подписки — с этим же именем, телефоном и источником в payload.

У лида из API нет ответственного менеджера

Лид, созданный через POST /leads, всегда создаётся без ответственного сотрудника — так же, как и лид без явно назначенного ответственного, созданный внутри самой CRM. Само по себе это не прячет лида: он виден в общем списке лидов школы (владельцу, администратору и любому сотруднику с доступом к разделу), просто без привязки к конкретному менеджеру. Проблема в другом — создание лида (через API или вручную) НЕ запускает автоматизацию этапа воронки (автозадачу и уведомление ответственному): она срабатывает только при явном переходе лида между статусами, а не в момент создания. Из-за этого лид может остаться незамеченным, если менеджеры ориентируются на задачи, а не на общий список.

Чтобы заявки из API гарантированно доходили до менеджера, настройте в CRM автоматизацию первого статуса той воронки, куда попадают лиды из API (обычно «Входящие» в «Основной воронке»): Воронки → выбранная воронка → статус → Автоматизация — задайте текст автозадачи и ответственного за неё. Для уже накопившихся лидов без задачи в этом статусе есть массовое действие «Применить автоматизацию ко всем сделкам в текущей стадии» — оно создаст задачи по текущим настройкам автоматизации, не дожидаясь ручного перемещения каждого лида между статусами.

Код: Node.js, обработчик формы сайта школы ​

js
const crypto = require('crypto')
const express = require('express')

const app = express()
app.use(express.json())

const BIGBEN_API = 'https://platformapi.bigbencrm.ru/api/public/v1'
const BIGBEN_API_KEY = process.env.BIGBEN_API_KEY // держите на сервере, не в браузере

app.post('/site/trial-lesson-form', async (req, res) => {
  const { name, phone, comment } = req.body

  const response = await fetch(`${BIGBEN_API}/leads`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${BIGBEN_API_KEY}`,
      'Idempotency-Key': crypto.randomUUID(),
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name,
      phone,
      comment: comment || undefined,
      source: 'site-form'
    })
  })

  const body = await response.json()

  if (response.status === 201) {
    return res.json({ ok: true, leadId: body.data.id })
  }

  if (response.status === 422 && body.error.code === 'validation_failed') {
    return res.status(422).json({ ok: false, fields: body.error.fields })
  }

  if (response.status === 429) {
    return res.status(503).json({ ok: false, message: 'Сервис занят, попробуйте ещё раз через минуту' })
  }

  return res.status(502).json({ ok: false, message: 'Не удалось передать заявку' })
})

app.listen(3000)

Совет

Держите BIGBEN_API_KEY только на бэкенде сайта школы (переменная окружения, секрет-хранилище). Ключ со скоупом write может создавать лидов, демо-уроков и зачисления в группы — попадание в браузер посетителя раскрывает его любому.