Онбординг разработчика: 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 с уже подставленным ключом:
# Этот 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:
# 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 в роутере:
-- миграция 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';// 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) — хорошая отправная точка.
- 429 —
RATE_LIMIT_EXCEEDEDпри превышении лимита per-minute или per-day. - 5xx — уникальный
request_id(та же UUID, что пишется вbtx_api_log), который разработчик приложит к обращению в поддержку. Без него диагностика на стороне поставщика занимает в десять раз больше времени.
Компактная выжимка кодов первого дня (полный список — на странице error-codes):
| Код | HTTP | Когда |
|---|---|---|
MISSING_API_KEY | 401 | Нет заголовка Authorization |
INVALID_API_KEY | 401 | Ключ не существует или подделан |
KEY_REVOKED | 401 | Ключ отозван |
INSUFFICIENT_SCOPE | 403 | У ключа нет нужного scope |
TRIAL_READ_ONLY | 403 | Триал — мутирующие методы запрещены |
RATE_LIMIT_EXCEEDED | 429 | Превышен лимит per-minute или per-day |
NO_CHANNEL_AVAILABLE | 422 | Не удалось определить канал для отправки |
CONTACT_NOT_FOUND | 422 | Контакт не найден или не принадлежит проекту |
Тезис 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 дней)Различия ровно в трёх местах, и все три — часть общей механики тарифов, а не специальные исключения:
- Лимиты тарифа. Поле
api_rate_per_minuteдля триала ниже, чем у платных (30 против 2000). Та же логика работает у платных клиентов, там значения выше. Разработчик на триале сталкивается ровно с тем же механизмом rate-limit, что и его пользователи на проде, просто с более жёстким порогом. Рядом —api_rate_per_day(от 1000 до 1000000 в зависимости от тарифа). - Toggle on-write операций. Флаг
api_write_enabledна триале равен 0: все POST/PUT/DELETE возвращают 403 с кодомTRIAL_READ_ONLY. Удобно для смоук-тестов, когда нужно прогнать чтение без риска что-то изменить. Тот же тогл доступен и платным клиентам. - Срок жизни. Триал — 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 оправдан в трёх случаях:
- Финансовые операции с реальным движением денег. Если каждый POST списывает деньги — нужен тестовый ключ эквайринга. Стандартное решение —
test_modeфлаг на уровне ключа: та же инфраструктура, но операции в тестовый шлюз. - Действия с побочными эффектами на третьих лиц. SMS, push, email на боевые номера — недопустимо. Нужен «test recipient» или режим, при котором сообщения логируются, но не доставляются.
- Тяжёлые операции с физическим миром. Промышленное оборудование, печать на завод, доставка грузов — тестировать на проде нельзя в принципе.
У нас все три обходятся флагами в самих модулях. Например, у 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-ключ — аналог пароля в открытом виде: у него нет ни срока действия по умолчанию, ни второго фактора, ни попыточного лимита. Любой, кто получит ключ, делает всё, что доступно тарифу проекта. Большинство утечек — не изощрённый взлом, а стандартные антипаттерны. Их семь:
- Ключ в репозитории. Разработчик пишет
$key = 'btx_live_...';в конфиге, коммитит. Через неделю репо становится публичным. Gitleaks-сканеры (GitHub Secret Scanning, GitLab Secret Detection) ловят паттернbtx_live_и шлют уведомление владельцу проекта — но между публикацией и отзывом проходят часы, за которые злоумышленник успевает выгрузить базу контактов.
- Ключ в
.envбез шифрования. Plaintext на диске. RCE-уязвимость, бэкап-копия, увольнённый админ — все читают разом. Конкретный пример: образ Docker, попавший в публичный реестр с забытым.envвнутри..env— приемлемое место для переменной, но не для хранения: ключ должен попадать туда из защищённого источника при деплое, а не лежать вечно.
- Один ключ на всю команду. Master-ключ в Slack, через два года ушли пять человек, ключ не ротировался. Любой бывший сотрудник может вернуться и навредить. Контрмера — один ключ на исполнителя (бот, воркер, сервис), не на человека: когда сервис выводится из эксплуатации, отзывается именно его ключ, остальные работают.
- Ключ в логах.
error_log('Request: ' . print_r($headers, true));— и полный заголовокAuthorization: Bearer XXXXXXXXоказывается в централизованной системе сбора (ELK, Loki, Datadog), где его видят все с доступом к мониторингу. SDK BOTIX (PHP, Python, Node) делают маскирование автоматически:
// 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;
}Если код пишется руками — это правило надо проговаривать в код-ревью.
- Ключ в URL.
https://api.example.com/contacts?api_key=...пишется в access.log nginx, в логах прокси и в Referer при переходах на третьи ресурсы — ключ размазан по всему сетевому стэку. Платформа BOTIX категорически не принимает ключ через query-string, только черезAuthorization: Bearer XXXXXXXX. Закреплено вapp/Middleware/PublicApiAuth.phpявной проверкой источника заголовка.
- Ключ без ротации. Выпущен два года назад, до сих пор работает. За это время сменилась половина команды, прошли два рефакторинга, был инцидент с подозрением на компрометацию («не точно, давайте оставим»). Ключ — бессрочный пароль; чем дольше живёт, тем выше шанс, что он уже утёк. Гигиена — плановая ротация раз в 6 месяцев плюс внеплановая по любому подозрению. Чтобы ротация была безболезненной, нужно поддерживать два состояния ключа: новый создан и работает, старый ещё не отозван; свитч в коде атомарный, отзыв старого — отложенный.
- Ключ с избыточными правами. Сервис, который только шлёт сообщения, использует ключ с полным набором прав — при компрометации злоумышленник удаляет контакты, меняет сценарии, читает журналы транзакций. Контрмера — 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 падает на старте с понятной ошибкой:
# Правильная инициализация
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 в продакшене нельзя:
.envв.gitignore.- В исходниках нет строк, начинающихся с
btx_live_(grep). - Конфиг читает ключ через
getenv()или эквивалент. - SDK инициализируется один раз на старте процесса, не на каждом запросе.
- В логах headers фильтруются — нет полного значения
Authorization. - URL'ы не содержат ключа.
- На каждый сервис — отдельный ключ с именем в метаданных (
btx_api_keys.name).
Одноразовый показ ключа и хранение только bcrypt-хеша означает, что потерянный ключ мы не восстановим — только перевыпустим (по дизайну). Отсюда кнопка «Перевыпустить» из тезиса 1: она аннулирует старый ключ и сразу выдаёт новый, не ломая правило «не хранить ключ в открытом виде в БД». Подробнее про подпись и доставку webhook, где X-Webhook-Secret хранится по тем же правилам, — в разборе webhooks: HMAC, retry и идемпотентность.
Попробуйте API BOTIX
Получите ключ, отправьте первый запрос и поймайте webhook — прямо в браузере, без боевого канала.
Попробовать API бесплатноЧитайте также
Quickstart, триал-sandbox и polling vs webhook
SDK BOTIX 1.1: zero-deps, генерация из OpenAPI и триал
Первый запрос к API за 5 минут: SDK из OpenAPI и триал
Публичный API BOTIX: OpenAPI, multi-tenant и Partner API
Почему в SDK ноль зависимостей и генерация из OpenAPI
