Сценарии использования
Два сквозных сценария от запроса до результата. Базовый адрес везде один: 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...Ответ:
{
"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 (секунды до следующей попытки) и тем же конвертом ошибки:
{ "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
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:
{
"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 с деталями по каждому полю:
{
"error": {
"code": "validation_failed",
"message": "...",
"fields": {
"phone": ["Поле phone обязательно для заполнения."]
}
}
}Показывайте посетителю сайта отдельное сообщение об ошибке для каждого поля из fields, не общий текст «что-то пошло не так» — так понятно, что именно исправить в форме.
Что видит школа в CRM
Лид сразу появляется в разделе лидов CRM в указанной воронке (или в «Основной», если funnel_id не передан), в первом статусе по приоритету. Отдельно от этого, если у школы настроена подписка на событие lead.created (см. Вебхуки), тот же лид одновременно уходит вебхуком на URL подписки — с этим же именем, телефоном и источником в payload.
Код: Node.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 может создавать лидов, демо-уроков и зачисления в группы — попадание в браузер посетителя раскрывает его любому.