Попробовать API
</>
Онбординг разработчика: quickstart, sandbox и API-ключи
sdkКейс
18 мин

Онбординг разработчика: quickstart, sandbox и API-ключи

TL;DR. Между регистрацией и первым успешным запросом у среднего разработчика уходило 20–30 минут — псевдокод вместо рабочего `curl`, тесты через боевой канал, «храните ключ безопасно» без конкретики. Разобрали три узла первого дня с Public API: что обязан содержать quickstart, почему developer-триал заменяет отдельный sandbox и как на самом деле хранить ключ. С рабочим кодом, SQL-миграцией, кодами ошибок BOTIX Public API v1 и чек-листом для код-ревью.

Коротко

  • Quickstart — отдельный жанр, а не короткая документация. Первый блок кода виден без скролла, ключ выдаётся на этой же странице, snippet содержит реальные значения. У разработчика 5–10 минут; после этого вкладка закрыта. Developer-триал обходит мастер настройки — одна колонка в БД и один if в роутере.
  • Отдельный sandbox почти всегда не нужен. Триал физически живёт на том же production-контуре, изолирован на уровне проекта и ограничен лимитами тарифа. Это убирает целый класс ошибок «в sandbox работало, на проде упало» и экономит команде десятки часов на синхронизацию контуров.
  • API-ключ — это пароль в открытом виде. Семь повторяющихся антипаттернов (репозиторий, .env, общий ключ, логи, URL, отсутствие ротации, избыточные права) закрываются одной схемой: secret manager → ENV → SDK, маскирование в логах, scopes, ротация. Плюс чек-лист код-ревью из семи проверок по минуте каждая.

Тезис 1. Quickstart за пять минут: что обязан содержать первый экран

Любой публичный API проходит через узкое горлышко — первую встречу с разработчиком. У неё нет второго шанса. Если человек тратит больше 5–10 минут на первый осмысленный запрос — он закрывает вкладку. Это не лень, это фокус: он пришёл проверить гипотезу «подходит ли этот API под мою задачу», и каждая лишняя минута увеличивает шанс, что гипотеза не будет проверена вовсе.

Большинство платформ относятся к quickstart как к сокращённой документации. Это ошибка. Документация отвечает на вопрос «как устроена система», её читают последовательно и возвращаются многократно. Quickstart отвечает на вопрос «как получить первый ответ прямо сейчас», его читают один раз и по диагонали. Разные жанры — разная структура.

Признаки правильного quickstart, которые легко проверить визуально:

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

В обычном онбординге BOTIX триал начинается с мастера: выбор ниши (барбершоп, доставка, медицина), заполнение профиля бизнеса, подключение Telegram, импорт каталога, настройка приветственного сценария. Это нужно владельцу бизнеса — его задача увидеть работающего бота под свою нишу. У разработчика задача другая: увидеть формат запросов и ответов, проверить, что подпись webhook совпадает с рассчитанной у него. Мастер для этой задачи — препятствие (как именно теряется разработчик в воронке первого дня — разбирали в Первом дне с API).

Реализация для curl — snippet берётся прямо с /developers/quickstart с уже подставленным ключом:

bash
# Этот snippet берётся прямо с /developers/quickstart с уже подставленным ключом.
# Ключ показывается один раз, потом скрывается за маской btx_live_****a3f7
curl -X POST https://api.botix.pro/public/v1/contacts \
  -H "Authorization: Bearer XXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"phone":"+79991234567","name":"Test"}'

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

python
# pip install botix==1.1.0
# Импортируем клиент. Один раз на старте процесса, не на каждый запрос.
from botix import Client

# Ключ читаем из ENV. В коде строкой — антипаттерн (тезис 3 ниже).
client = Client(api_key=XXXXXXXX['BOTIX_API_KEY'])

# SDK сам генерирует Idempotency-Key на каждый мутирующий метод.
contact = client.contacts.create(
    phone="+79991234567",
    name="Test",
)
print(contact.id, contact.created_at)

Заголовок Idempotency-Key поддерживается на мутирующих endpoints (POST /messages, POST /scenarios/{id}/run): кеш в Redis на 24 часа по ключу papi:idem:{apiKeyId}:{key}, при повторе возвращается тот же результат с заголовком Idempotent-Replayed: 1. Кешируется только успех (200/201), не ошибки доставки — поэтому один и тот же ключ идемпотентности нужно менять на новый календарный день.

Что отрезано из quickstart осознанно, чтобы он не раздулся до 500 строк и не потерял жанр:

  • Все возможные коды ошибок → отдельная страница error-codes.
  • Webhook-подписи → отдельный гайд по webhooks.
  • OAuth → отдельный flow для third-party-приложений.
  • Идемпотентность → отдельный раздел best-practices.
  • Версионирование → отдельная страница versioning.

Каждая тема критична для production, но не для первого запроса. Quickstart решает вопрос «брать или не брать», как библиотечная аннотация книги — не пересказывает сюжет.

Одноразовый показ ключа — компромисс. Правильная практика безопасности: показать ключ один раз, дальше хранить только bcrypt-хеш (cost 12). Если база утечёт, восстановить рабочие ключи невозможно. Но для quickstart это трение: разработчик скопировал ключ, получил 401, переключился между окнами, потерял ключ — и нужно перегенерировать. Компромисс: ключ показывается один раз на странице quickstart, но рядом — кнопка «Перевыпустить», которая аннулирует старый и сразу показывает новый. Правило «не хранить ключ в открытом виде в БД» сохраняется, трение убирается.

Единственная полезная метрика для quickstart — медианное время от просмотра страницы до первого успешного API-запроса с того же ключа. Меньше 5 минут — quickstart выполняет работу. Больше — где-то трение, ищется через сессионные логи. Число просмотров, время на странице, прокрутка — вторичны: quickstart нужен не для чтения, а для того, чтобы после него разработчик сделал работающий запрос.

Как устроить developer-триал: одна колонка в БД и один if

При регистрации ставим пометку intent: is_developer_trial. Если флаг есть, система обходит мастер и сразу открывает /developers/quickstart: выданный API-ключ, четыре snippet-таба (curl, PHP, Python, Node), ссылки на документацию и Swagger UI. Архитектурно — одна строка в БД и один if в роутере:

sql
-- миграция 2026_05_22_developer_trial_flag.sql
ALTER TABLE btx_projects ADD COLUMN is_developer_trial TINYINT(1) DEFAULT 0;
ALTER TABLE btx_users ADD COLUMN registration_intent ENUM('owner','developer') DEFAULT 'owner';
php
// app/Controllers/OnboardingController.php (упрощённо)
if ($user->registration_intent === 'developer') {
    return redirect('/developers/quickstart');
}
return redirect('/setup');  // обычный мастер

Триал остался тем же — тот же тариф, та же БД, те же лимиты. Изменилась только точка приземления. Среднее время до первого запроса упало до 3–5 минут.

Что должно быть в первом ответе API

Первый запрос разработчика почти всегда заканчивается одним из четырёх исходов: 200, 401, 422, 5xx. Все четыре одинаково важны, и каждый требует внятной подсказки. Ответ приходит в едином формате {success, data, meta} при успехе или {success: false, error: {code, message, details}} при ошибке.

  • 200 — возвращённый объект содержит достаточно полей, чтобы было видно, что произошло: не {"id": 123}, а полный объект с временем создания, статусом и ссылкой на детальный endpoint.
  • 401 — ответ явно говорит, что не так с ключом. В BOTIX Public API это три разных кода, а не один общий: MISSING_API_KEY (нет заголовка Authorization), INVALID_API_KEY (ключ не существует или подделан), KEY_REVOKED (ключ отозван).
  • 403 — права. INSUFFICIENT_SCOPE (у ключа нет нужного scope), API_NOT_AVAILABLE_ON_PLAN (тариф не включает API), TRIAL_READ_ONLY (триал, мутирующие методы запрещены).
  • 422 — список ошибок валидации по полям. Стандарт RFC 7807 (Problem Details for HTTP APIs) — хорошая отправная точка.
  • 429RATE_LIMIT_EXCEEDED при превышении лимита per-minute или per-day.
  • 5xx — уникальный request_id (та же UUID, что пишется в btx_api_log), который разработчик приложит к обращению в поддержку. Без него диагностика на стороне поставщика занимает в десять раз больше времени.

Компактная выжимка кодов первого дня (полный список — на странице error-codes):

КодHTTPКогда
MISSING_API_KEY401Нет заголовка Authorization
INVALID_API_KEY401Ключ не существует или подделан
KEY_REVOKED401Ключ отозван
INSUFFICIENT_SCOPE403У ключа нет нужного scope
TRIAL_READ_ONLY403Триал — мутирующие методы запрещены
RATE_LIMIT_EXCEEDED429Превышен лимит per-minute или per-day
NO_CHANNEL_AVAILABLE422Не удалось определить канал для отправки
CONTACT_NOT_FOUND422Контакт не найден или не принадлежит проекту

Тезис 2. Триал вместо отдельного sandbox

Первая итерация триала требовала: подключите Telegram-бот, получите токен у BotFather, вставьте в кабинет, проверьте webhook — и только потом первый запрос. Абсурд с точки зрения разработчика: он не хочет подключать боевой канал, он хочет проверить, что API существует, отвечает в нужном формате и совместим с его кодом.

Когда у платформы появляется публичный API, первое решение почти автоматическое: «нужен sandbox» — отдельная копия инфраструктуры, отдельный домен (sandbox.api.example.com), отдельная база, отдельные ключи, отдельный CI. Логика понятная: дать разработчику место, где он ломает всё что хочет. Проблема в том, что любой второй контур — это не половина прода, а отдельный продукт, и у него своя жизнь, которая со временем расходится с жизнью прода.

Решение BOTIX — триал стал полноценным sandbox без отдельной инфраструктуры:

┌──────────────────────────────────────────┐
│  Триал-проект                            │
│  ─ та же база, что у платных             │
│  ─ те же FPM-воркеры                     │
│  ─ те же FastAPI-сервисы                 │
│  ─ те же Redis-очереди                   │
│  ─ те же миграции, та же логика ошибок   │
└──────────────────────────────────────────┘
       Отличия только в:
       ─ значение api_rate_per_minute (30 вместо 2000)
       ─ флаг api_write_enabled (можно отключить)
       ─ срок жизни (14 дней)

Различия ровно в трёх местах, и все три — часть общей механики тарифов, а не специальные исключения:

  1. Лимиты тарифа. Поле api_rate_per_minute для триала ниже, чем у платных (30 против 2000). Та же логика работает у платных клиентов, там значения выше. Разработчик на триале сталкивается ровно с тем же механизмом rate-limit, что и его пользователи на проде, просто с более жёстким порогом. Рядом — api_rate_per_day (от 1000 до 1000000 в зависимости от тарифа).
  2. Toggle on-write операций. Флаг api_write_enabled на триале равен 0: все POST/PUT/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 и привязанным тарифом «триал». API-ключ триала видит только данные своего проекта — как и любой другой ключ. Разработчик может делать с API всё что угодно (создавать контакты, отправлять webhook, перегружать запросами), но воздействует только на свой проект. Чужие данные защищены той же логикой Auth::assertProjectAccess(), что защищает их у платных клиентов. Идея «безопасной среды» реализуется через изоляцию проекта, а не изоляцию инфраструктуры — это правильный уровень (тот же принцип, что и в разборе multi-tenant и cross-project защиты).

Где подход неприменим. Не любой API подходит под такую модель. Отдельный sandbox оправдан в трёх случаях:

  1. Финансовые операции с реальным движением денег. Если каждый POST списывает деньги — нужен тестовый ключ эквайринга. Стандартное решение — test_mode флаг на уровне ключа: та же инфраструктура, но операции в тестовый шлюз.
  2. Действия с побочными эффектами на третьих лиц. SMS, push, email на боевые номера — недопустимо. Нужен «test recipient» или режим, при котором сообщения логируются, но не доставляются.
  3. Тяжёлые операции с физическим миром. Промышленное оборудование, печать на завод, доставка грузов — тестировать на проде нельзя в принципе.

У нас все три обходятся флагами в самих модулях. Например, у bot-preview есть $context['preview']=true — модуль пишет в лог, но не отправляет реальное сообщение в Telegram. Снимает риск побочных эффектов без поднятия второго контура.

Чем платит команда за отдельный sandbox

Расходов больше, чем кажется на старте: дублирование инфраструктуры (свои серверы БД, очередей, кеша — если экономить, sandbox медленный и тайминги отличаются), дублирование миграций (любая фича попадает в оба контура, реальная команда забывает, через полгода тикеты «синхронизируем sandbox»), дублирование тестовых данных, и самое болезненное — расхождение поведения (sandbox считается «безопасным», в нём ослабляют проверки, интеграция работает в sandbox и падает на проде).

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

На горизонте года экономия времени команды — десятки часов, которые ушли бы на синхронизацию контуров и разбор тикетов о расхождениях. Главное условие: лимиты должны быть продуманными, а флаги is_trial / write_enabled — не исключениями, а частью общей механики тарифов, работающей у всех клиентов одинаково.

Тезис 3. Хранение API-ключа: семь антипаттернов

Документация первой версии содержала фразу «храните API-ключ в безопасном месте». Бесполезный совет: разработчик не знает, что считается «безопасным местом», и кладёт ключ туда, куда удобнее — в .env, в конфиг, в git. API-ключ — аналог пароля в открытом виде: у него нет ни срока действия по умолчанию, ни второго фактора, ни попыточного лимита. Любой, кто получит ключ, делает всё, что доступно тарифу проекта. Большинство утечек — не изощрённый взлом, а стандартные антипаттерны. Их семь:

  1. Ключ в репозитории. Разработчик пишет $key = 'btx_live_...'; в конфиге, коммитит. Через неделю репо становится публичным. Gitleaks-сканеры (GitHub Secret Scanning, GitLab Secret Detection) ловят паттерн btx_live_ и шлют уведомление владельцу проекта — но между публикацией и отзывом проходят часы, за которые злоумышленник успевает выгрузить базу контактов.
  1. Ключ в .env без шифрования. Plaintext на диске. RCE-уязвимость, бэкап-копия, увольнённый админ — все читают разом. Конкретный пример: образ Docker, попавший в публичный реестр с забытым .env внутри. .env — приемлемое место для переменной, но не для хранения: ключ должен попадать туда из защищённого источника при деплое, а не лежать вечно.
  1. Один ключ на всю команду. Master-ключ в Slack, через два года ушли пять человек, ключ не ротировался. Любой бывший сотрудник может вернуться и навредить. Контрмера — один ключ на исполнителя (бот, воркер, сервис), не на человека: когда сервис выводится из эксплуатации, отзывается именно его ключ, остальные работают.
  1. Ключ в логах. error_log('Request: ' . print_r($headers, true)); — и полный заголовок Authorization: Bearer XXXXXXXX оказывается в централизованной системе сбора (ELK, Loki, Datadog), где его видят все с доступом к мониторингу. SDK BOTIX (PHP, Python, Node) делают маскирование автоматически:
php
// github.com/BOTIX-pro/sdk-php/blob/v1.1.0/src/Http/RequestLogger.php
private function sanitizeHeaders(array $headers): array {
    foreach (['Authorization', 'X-API-Key', 'X-Webhook-Secret'] as $key) {
        if (isset($headers[$key])) {
            $headers[$key] = '***';
        }
    }
    return $headers;
}

Если код пишется руками — это правило надо проговаривать в код-ревью.

  1. Ключ в URL. https://api.example.com/contacts?api_key=... пишется в access.log nginx, в логах прокси и в Referer при переходах на третьи ресурсы — ключ размазан по всему сетевому стэку. Платформа BOTIX категорически не принимает ключ через query-string, только через Authorization: Bearer XXXXXXXX. Закреплено в app/Middleware/PublicApiAuth.php явной проверкой источника заголовка.
  1. Ключ без ротации. Выпущен два года назад, до сих пор работает. За это время сменилась половина команды, прошли два рефакторинга, был инцидент с подозрением на компрометацию («не точно, давайте оставим»). Ключ — бессрочный пароль; чем дольше живёт, тем выше шанс, что он уже утёк. Гигиена — плановая ротация раз в 6 месяцев плюс внеплановая по любому подозрению. Чтобы ротация была безболезненной, нужно поддерживать два состояния ключа: новый создан и работает, старый ещё не отозван; свитч в коде атомарный, отзыв старого — отложенный.
  1. Ключ с избыточными правами. Сервис, который только шлёт сообщения, использует ключ с полным набором прав — при компрометации злоумышленник удаляет контакты, меняет сценарии, читает журналы транзакций. Контрмера — scopes на уровне ключа. У BOTIX scopes — JSON-поле в btx_api_keys и btx_oauth_access_tokens. При создании ключа на app.botix.pro/developers указывается список scope, middleware проверяет соответствие на каждом запросе. Сервису, которому нужны только сообщения, выдают ключ с messages:send — при компрометации никаких побочных эффектов.

Нормальная схема хранения ключа

Источник правды — secret manager (HashiCorp Vault, AWS Secrets Manager, Yandex Lockbox, в простом случае — pass поверх gpg). Ключ хранится там зашифрованным, доступ дают ролям, есть audit-лог. При деплое скрипт читает ключ из secret manager и подставляет в окружение процесса — переменную ENV. Файл .env в репозитории отсутствует или содержит только placeholder'ы. Контейнер пересобирается без ключа, инжектится при docker run -e BOTIX_API_KEY=... или через Kubernetes Secret. SDK читает ключ из ENV. Если ENV не установлен — SDK падает на старте с понятной ошибкой:

python
# Правильная инициализация
import os
from botix import Client

api_key = XXXXXXXX('BOTIX_API_KEY')
if not api_key:
    # Падаем при старте процесса, не при первом запросе.
    raise RuntimeError("BOTIX_API_KEY env var is required")

client = Client(api_key=api_key)

Последний обязательный элемент — audit-лог. На app.botix.pro/developers журнал API показывает, какие запросы шли с каждого ключа. Если ключ внезапно начал делать DELETE /contacts, хотя в норме делает только POST /messages, — это видно сразу, и его можно отозвать до того, как ущерб станет масштабным.

Чек-лист для код-ревью

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

  1. .env в .gitignore.
  2. В исходниках нет строк, начинающихся с btx_live_ (grep).
  3. Конфиг читает ключ через getenv() или эквивалент.
  4. SDK инициализируется один раз на старте процесса, не на каждом запросе.
  5. В логах headers фильтруются — нет полного значения Authorization.
  6. URL'ы не содержат ключа.
  7. На каждый сервис — отдельный ключ с именем в метаданных (btx_api_keys.name).

Одноразовый показ ключа и хранение только bcrypt-хеша означает, что потерянный ключ мы не восстановим — только перевыпустим (по дизайну). Отсюда кнопка «Перевыпустить» из тезиса 1: она аннулирует старый ключ и сразу выдаёт новый, не ломая правило «не хранить ключ в открытом виде в БД». Подробнее про подпись и доставку webhook, где X-Webhook-Secret хранится по тем же правилам, — в разборе webhooks: HMAC, retry и идемпотентность.