Попробовать API
</>
Quickstart, триал-sandbox и polling vs webhook
sdkКейс
15 мин

Quickstart, триал-sandbox и polling vs webhook

TL;DR: свой developer experience мы переделали три раза за полгода. Переписали quickstart с нуля, отказались от отдельной песочницы в пользу триала на проде, неделю спорили внутри команды про polling и webhook. Ниже — три решения с кодом, эндпоинтами и заголовками ровно как они работают в Public API v1.1.0, и что сделали бы иначе.

Коротко

  • Quickstart — отдельный жанр, не сокращённая документация. Первый блок кода без скролла, ключ выдаётся тут же, реальные значения вместо YOUR_API_KEY, между copy и результатом ноль шагов. Метрика одна: медиана «страница → первый успешный запрос».
  • Триал = sandbox на том же production-контуре. Отдельного sandbox.api.* нет: те же воркеры, миграции, валидаторы, заголовки. Различия в трёх флагах тарифа — api_rate_per_minute, api_write_enabled, срок 14 дней. Класс ошибок «в sandbox работало, в prod нет» исчезает.
  • Webhook по умолчанию, polling — честный fallback. Webhook через POST /public/v1/webhooks, HMAC-SHA256 в X-Botix-Signature, retry 30с/5м/30м/2ч/6ч. Polling через cursor — для локалки без туннеля и cron-jobs, явно помечен fallback в доке.

BOTIX — SaaS-платформа чат-ботов, команда небольшая. Публичный REST API выпустили в этом году (сейчас api.botix.pro/public/v1, версия 1.1.0), до этого был внутренний «для своих». Когда стали готовиться к открытию для внешних разработчиков, поняли неприятное: внутренняя документация, по которой работали сами, непригодна для человека с улицы. Знакомая ситуация для любой команды, делавшей API «сначала для себя, потом для других». Ниже — три решения по developer experience: где ошиблись, что переделали, что оставили бы как есть. Смежный разбор про грабли онбординга — в статье Онбординг разработчика: 5 граблей и решения.

Тезис 1. Quickstart, который не дочитывали

Первая версия quickstart была написана как сокращённый гайд — текст на четыре экрана: что такое API, концепции, авторизация, регистрация, получение ключа, первый запрос. Через две недели посмотрели в аналитику: медианное время на странице — 47 секунд, до блока с первым curl докручивали около 30%, реально выполняли запрос — единицы.

Поговорили с тремя разработчиками. Все трое сказали примерно одно: «пришёл проверить, подходит ли API под мою задачу. Мне нужно за минуту увидеть, как делается запрос. У вас сначала пять абзацев теории. Закрыл вкладку». Это не лень — это фокус: разработчик проверяет гипотезу «подходит ли API», и каждая лишняя минута увеличивает шанс, что гипотеза не будет проверена вовсе.

Вывод, который переломил подход: quickstart — это не короткая версия документации, а отдельный жанр. Документация отвечает на вопрос «как устроена система», её читают последовательно и возвращаются. Quickstart отвечает на вопрос «как получить первый ответ от API прямо сейчас», его читают один раз и по диагонали.

Переписали по проверяемым визуально правилам:

  • Первый блок кода виден без скролла, на первом экране после заголовка.
  • API-ключ выдаётся прямо здесь, не «зарегистрируйтесь, подтвердите email, дождитесь письма, перейдите в кабинет, создайте проект, найдите вкладку API».
  • Snippet содержит реальные значения, не плейсхолдеры YOUR_API_KEY или https://api.example.com.
  • Переключатель языка: curl, PHP, Python, Node.
  • Между копированием кода и видимым результатом — ноль промежуточных шагов.

В обычном онбординге BOTIX триал начинается с мастера: выбор ниши, профиль бизнеса, подключение Telegram, импорт каталога, приветственный сценарий. Это нужно владельцу бизнеса. У разработчика задача другая — понять форматы запросов и ответов, проверить, что подпись webhook совпадает с расчётом на своей стороне. Мастер для этой задачи — препятствие. Поэтому при регистрации с пометкой «я разработчик» (флаг is_developer_trial в проекте) система обходит мастер и сразу открывает /developers/quickstart: выданный ключ показан один раз, рядом четыре snippet-таба и ссылки на полную доку и Swagger UI.

bash
curl -X POST https://api.botix.pro/public/v1/contacts \
  -H "Authorization: Bearer XXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79991234567","name":"Test"}'

Тот же запрос в Python через SDK:

python
from botix import Client

client = Client(api_key="XXXXXXXX")
contact = client.contacts.create(phone="+79991234567", name="Test")

Никаких пяти абзацев теории, никакого выбора ниши. Хочет концепции — есть отдельная страница «Architecture». Нужны error-codes — отдельная. Webhook — отдельная. Quickstart только про «сделать первый запрос и увидеть ответ».

Одноразовый показ ключа — компромисс. Полный ключ (btx_live_<40 символов>) показывается один раз, в БД хранится только bcrypt-хеш (cost 12), сравнение через password_verify + hash_equals. Правильно с точки зрения безопасности, но создаёт трение: скопировал, получил 401, переключил окна, потерял ключ. Поэтому рядом — кнопка «Перевыпустить»: аннулирует старый ключ и сразу показывает новый. Правило «не хранить ключ в открытом виде» сохранено, трение для тестовых сценариев убрано.

Что должно быть в первом ответе. Первый запрос почти всегда заканчивается одним из четырёх исходов, и каждый требует подсказки:

  • 200 — полный объект (id, время создания, статус, ссылка на детальный endpoint), а не {"id": 123}.
  • 401 — что не так с ключом: отсутствует, отозван, нет нужного scope (403 INSUFFICIENT_SCOPE, если scope не тот).
  • 422 — список ошибок валидации по полям с конкретным полем; RFC 7807 как отправная точка.
  • 5xx — уникальный request_id в заголовке X-Botix-Request-Id, который можно приложить к обращению в поддержку.

Метрика, которая отвечает на вопрос «работает ли quickstart». Единственный полезный показатель — медиана времени от просмотра страницы до первого успешного запроса с того же ключа. Меньше пяти минут — работает. Больше — ищем трение через сессионные логи. Число просмотров, время на странице, глубина скролла вторичны: quickstart нужен не чтобы его читали, а чтобы после него сделали работающий запрос. Про то, почему сам SDK при этом должен генерироваться из OpenAPI, а не писаться руками — в разборе SDK из OpenAPI: zero-deps и генерация клиента.

Тезис 2. Песочница, которой не стало

Параллельно обсуждали отдельный sandbox-домен sandbox.api.botix.pro со своей базой, ключами, панелью, CI-пайплайном. «Дать безопасную среду, где можно ломать всё что угодно» звучит правильно. Начали проектировать, считали инфраструктуру, миграции, синхронизацию тестовых данных.

На третьем дне один из ребят задал вопрос, который перевернул обсуждение: почему мы предполагаем, что разработчик хочет тестировать на копии нашей системы? Он хочет тестировать на самой системе.

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

  • Дублирование инфраструктуры. Sandbox требует своих БД, очередей, кеша, воркеров. Экономишь — sandbox отдаёт другие тайминги, тесты становятся ненадёжными.
  • Дублирование миграций и фич. Каждая фича — в оба контура. Команда забывает. Через полгода в sandbox устаревшие поля, неработающие endpoint'ы, отдельный тикет «синхронизировать sandbox с prod».
  • Дублирование тестовых данных. Набор тестовых сущностей устаревает: формат меняется, ссылочная целостность ломается на наполовину применённых миграциях.
  • Расхождение поведения. Самый болезненный пункт. Sandbox считается «безопасным», поэтому в нём ослабляют rate-limit, выключают fraud-фильтры, смягчают валидацию. Интеграция работает в sandbox, переключается на prod — падает на первом запросе. Классический «у меня работало в sandbox» — не лень разработчика, а архитектурное расхождение, заложенное командой платформы.

Песочницу не сделали. Вместо неё — триальный кабинет на том же production-контуре. Триальный проект пишет в ту же базу, идёт через те же FPM-воркеры, те же FastAPI-сервисы, ту же Redis-очередь, те же миграции, ту же логику обработки ошибок, те же коды и заголовки в ответе. Различия только в трёх местах:

  1. Лимиты тарифа. Поле api_rate_per_minute для триала — 30 запросов в минуту (у платных — до 2000, api_rate_per_day — от 1000 до 1000000). Разработчик сталкивается ровно с тем же механизмом rate-limit, что и его пользователи на проде, просто с более жёстким порогом. Механизм один — RateLimiter::check($apiKeyId, $projectId, $method).
  2. Toggle on-write операций. У триала api_write_enabled = 0 — кабинет в read-only, все POST/PUT/PATCH/DELETE возвращают 403 TRIAL_READ_ONLY с понятным кодом. Удобно для смоук-тестов: прогнать чтение без риска что-то изменить. Тот же тогл доступен и платным клиентам.
  3. Срок жизни — 14 дней, после — архивное состояние (read-only до оплаты). Не успел — заводишь второй триал или оплачиваешь минимальный тариф.

Почему это работает. Главный аргумент: разработчик тестирует ту же систему, которую увидит на проде. Не её модель, не урезанную копию, не сборку трёхмесячной давности. Это убирает целый класс ошибок:

  • «Поле было в sandbox, в проде нет» — невозможно, контур один.
  • «В sandbox 200, в проде 422» — невозможно, валидаторы те же.
  • «Webhook в sandbox отправляется, в проде нет» — невозможно, код обработчика тот же.
  • «Заголовок X-RateLimit-* в sandbox другого формата» — невозможно, middleware один.

Бонус: команда тратит ноль усилий на синхронизацию контуров, потому что синхронизировать нечего.

Где живёт граница. Не в инфраструктуре, а в данных и правах. Триальный проект — просто запись в btx_projects с флагом is_developer_trial = true и тарифом «триал» со своими значениями в JSON limits. API-ключ триала видит только данные своего проекта, как и любой другой ключ: та же Auth::assertProjectAccess($projectId) защищает чужие данные у всех. При запросе чужого ресурса — 404, не 403 (не утекает существование объектов). Разработчик может ломать что угодно — но только в своём проекте.

Где подход неприменим. Честно: не для любого API. Отдельный sandbox оправдан, когда:

  • Реальное движение денег. Если POST к /payments/charge списывает деньги — нужен test-mode-ключ, отправляющий операции в тестовый эквайринговый шлюз.
  • Побочные эффекты на третьих лиц. SMS/push/email на боевые номера клиентов недопустимы — нужен «test recipient» или режим «логировать, но не доставлять».
  • Физический мир. Промышленное оборудование, печать на завод, доставка грузов — только dev/staging/prod.

В BOTIX все три случая обходятся флагами в самих модулях: у bot-preview есть $context['preview']=true, который заставляет модуль писать в лог, но не отправлять реальное сообщение в Telegram. Риск побочек снят без второго контура.

Стоимость поддержки — таблица.

Затратаdev/staging/prodТриал-как-sandbox
Серверых2-х3х1
МиграцииПрименять дваждыОдин раз
Тестовые данныеПоддерживать отдельноНе нужны
Документация«Различия sandbox и prod»Не нужна
Тикеты «в sandbox работает, в prod нет»РегулярноНе возникают

Что бы сделали иначе: возможно, всё же добавили бы отдельный test-mode-ключ — на случай клиентов, для которых триал с 30 RPM окажется недостаточной изоляцией. В плане. Главное условие модели — флаги is_developer_trial / api_write_enabled должны быть не специсключениями, а частью общей механики тарифов, работающей у всех клиентов одинаково.

Тезис 3. Polling vs webhook — сделали оба

Третий перелом — про доставку событий. Polling — клиент сам периодически опрашивает API «дай всё, что появилось после отметки». Webhook — мы шлём HTTP в момент события. Внешне разница мала, но в проде это разная нагрузка, разная задержка, разные требования к своей инфраструктуре. Внутренний спор шёл больше недели.

Что такое polling и чем платит. Простейший вид — GET с курсором, отвечающий списком новых событий. Клиент запоминает позицию, ждёт N секунд, делает следующий запрос. Достоинство — простота: работает без публичного endpoint'а, без HTTPS-сертификата, без обработки повторных доставок. Воркер polling'а — маленький while-loop с requests.get и cursor'ом; запустится на localhost, в Docker, в Lambda без настройки сети. Недостаток — обратная сторона простоты: период опроса = лаг. Раз в секунду — лаг до 1 с, но 60 запросов в минуту на ключ и 86 400 в сутки. Раз в минуту — лаг до 60 с, для интерактивного бота уже неприемлемо. Малый лаг и большой запас по rate-limit одновременно не удержать.

Что такое webhook и чем платит. HTTP-запрос, который платформа шлёт на endpoint клиента в момент события. Задержка минимальная — сетевой round-trip плюс обработка, обычно десятки-сотни миллисекунд, никаких пустых запросов. Цена — инфраструктура: публичный endpoint, HTTPS с валидным сертификатом, обработка повторных доставок (платформа вправе ретраить при таймауте), проверка подписи на каждом запросе, тайм-аут ответа в пределах нескольких секунд. На localhost — только через ngrok, и это уже не «маленький while-loop».

Решили не голосованием, а прагматичным наблюдением: у каждого способа своя сфера, навязывать один за счёт другого — ошибка. Сделали оба и явно расставили приоритеты.

Webhook — основной паттерн. Регистрация подписки через POST /public/v1/webhooks (scope webhooks:write) либо в кабинете на app.botix.pro/developers. События эмитятся из public-контроллеров при мутирующих операциях: contact.created, contact.updated, contact.deleted, contact.tagged, message.sent, scenario.started (остальные — message.received, chat.opened, scenario.completed — подключаются из bot-движка). Доставка через WebhookDispatcher с очередью в БД (btx_api_webhook_deliveries) и cron-диспетчером раз в минуту. Встроенный retry по расписанию 30 секунд / 5 минут / 30 минут / 2 часа / 6 часов, максимум 5 попыток — потом статус failed и видно в delivery dashboard.

Подпись — HMAC-SHA256 от тела запроса на общем секрете подписки, в заголовке X-Botix-Signature как sha256=…. Рядом идут X-Botix-Event и X-Botix-Request-Id. Проверка на стороне клиента:

python
import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)  # X-Botix-Signature

Polling — fallback через cursor. Эндпоинты GET /public/v1/chats, GET /public/v1/messages, GET /public/v1/contacts поддерживают query-параметр cursor и возвращают meta.next_cursor (параллельно со старыми page/per_page — старые клиенты не ломаются). Клиент сохраняет последний cursor, опрашивает периодически, идёт вперёд до пустого next_cursor:

python
cursor = None
while True:
    params = {"cursor": cursor} if cursor else {}
    r = client.get("/public/v1/messages", params=params)
    for msg in r["data"]:
        handle(msg)
    cursor = r["meta"]["next_cursor"]
    if not cursor:
        break
    time.sleep(30)  # 30-60 с на время разработки

Это покрывает сценарии, где webhook поднять невозможно: локальная разработка без туннеля, jobs из CRM по своему расписанию. В документации developers.botix.pro/best-practices polling явно отмечен как fallback: «webhook по умолчанию, polling если webhook невозможен». Сознательное руководство, не нейтральная позиция. Причина: при росте числа клиентов один опрашивающий воркер быстро упрётся в rate-limit (X-RateLimit-Limit per minute зависит от тарифа), а растягивать период опроса нельзя без потери качества.

Как выбрать на старте. Простой алгоритм для чат-интеграции:

  • Стабильный публичный endpoint и нужна задержка < 5 секунд → webhook, без раздумий.
  • Разработка на локальной машине, публичный endpoint поднять сложно → polling через cursor с периодом 30-60 с на время разработки, переход на webhook в момент выхода в прод.
  • Запуск из cron-job раз в час → polling, webhook здесь бессмысленен.

И отдельный пункт: никогда не сравнивайте polling с webhook по «удобству на старте», не сделав замер ожидаемого трафика и допустимой задержки. Polling кажется проще на MVP, но при масштабировании первым ломается. Про то, как один SDK держит polling сразу на 100 ботов агентства без превышения лимитов — в разборе Polling, multi-tenant и retry на масштабе.

Что бы сделали иначе: добавили бы periodic reconciliation как третий гибридный паттерн — webhook основной, polling раз в час для сверки локального состояния с состоянием платформы. Если webhook потерялся (таймаут, ошибка обработчика, которую не заметили вовремя), reconciliation подберёт пропущенные события. Особенно полезно, когда последствия пропущенного события дорогие — например, не доставили триггерное сообщение по платёжному статусу. Пока в плане.

Что в итоге

Один продукт переделали три раза за полгода — нормальная история для маленькой команды, впервые выпускающей публичный API. Главное — не строить из себя «всё было задумано так с самого начала»:

  • Сначала написали quickstart как сокращённую документацию, увидели метрики, переписали полностью.
  • Сначала собирались делать отдельный sandbox, на середине проектирования отказались в пользу триала на проде.
  • Сначала спорили о polling и webhook как «или-или», в итоге сделали оба и явно обозначили приоритеты.

Quickstart не должен пересказывать сюжет. Песочница не должна быть копией прода. Polling не должен быть «более простым выбором» — он должен быть честным fallback'ом для случаев, где webhook невозможен.