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 — обработка лимита 120 запросов в минуту

При превышении лимита (120 запросов в минуту на один ключ) 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 в следующем запуске — так в выборку попадут и записи, изменённые ровно во время текущего прогона.

Код: 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;
    }

    /** @return array{data: array, meta?: array} */
    private function request(string $path): array
    {
        $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) {
            sleep($this->retryAfterSeconds($headers));
            return $this->request($path);
        }

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

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

    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, без промежуточного бэкенда школы, который сам ведёт базу лидов.

Шаг 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.

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

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

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

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

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

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

Код: 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 может создавать лидов, демо-уроков и зачисления в группы — попадание в браузер посетителя раскрывает его любому.