Best Practices
Практические рекомендации по работе с публичным API BOTIX. Код-снипеты — на PHP, Python и Node.js. Берите как образец и адаптируйте под ваш стек.
2. Идемпотентность на стороне клиента
3. Подписка на webhook: подпись + защита от replay
4. Пагинация: cursor vs page
5. Безопасное хранение API-ключей
1. Retry с экспоненциальным backoff на 429/5xx
API возвращает 429 RATE_LIMIT_EXCEEDED при превышении лимита (с заголовком Retry-After) и 5xx при временных сбоях на нашей стороне. Корректный клиент должен повторить запрос, но не сразу — иначе попадёт в чёрный список IP по подозрению на флуд.
Алгоритм:
- Базовая задержка перед первым ретраем — секунда. Каждый следующий — ×2.
- Добавляйте jitter ±20% — разрозим момент ретрая у одновременно упавших клиентов.
- Если в ответе есть
Retry-After— он перебивает расчётную задержку. - Максимум 5 попыток. Дальше — операцию в очередь или ошибка пользователю.
- Ретраить только идемпотентные запросы (GET) и мутирующие только с
Idempotency-Key.
function botixCall(string $url, array $body, string $key, int $attempt = 0): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $key,
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$hdrSz = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$hdrs = substr($raw, 0, $hdrSz);
$body = substr($raw, $hdrSz);
curl_close($ch);
if (($status === 429 || $status >= 500) && $attempt < 5) {
$retryAfter = 0;
if (preg_match('/^Retry-After:\s*(\d+)/mi', $hdrs, $m)) {
$retryAfter = (int) $m[1];
}
$wait = max($retryAfter, pow(2, $attempt)) * (0.8 + lcg_value() * 0.4);
usleep((int) ($wait * 1_000_000));
return botixCall($url, $body, $key, $attempt + 1);
}
return ['status' => $status, 'body' => json_decode($body, true)];
}import os, time, uuid, random, requests
def botix_call(url: str, body: dict, key: str, attempt: int = 0):
headers = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
}
r = requests.post(url, json=body, headers=headers, timeout=15)
if (r.status_code == 429 or r.status_code >= 500) and attempt < 5:
retry_after = int(r.headers.get("Retry-After", 0))
wait = max(retry_after, 2 ** attempt) * (0.8 + random.random() * 0.4)
time.sleep(wait)
return botix_call(url, body, key, attempt + 1)
return rimport { randomUUID } from "crypto";
export async function botixCall(url, body, key, attempt = 0) {
const r = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify(body),
});
if ((r.status === 429 || r.status >= 500) && attempt < 5) {
const retryAfter = Number(r.headers.get("Retry-After") || 0);
const wait = Math.max(retryAfter, 2 ** attempt) * (0.8 + Math.random() * 0.4);
await new Promise(res => setTimeout(res, wait * 1000));
return botixCall(url, body, key, attempt + 1);
}
return r;
}2. Идемпотентность на стороне клиента
Заголовок Idempotency-Key защищает мутирующие операции от дублей при сетевых сбоях. Сетевой запрос упал по таймауту — повторите его с тем же ключом, и BOTIX вернёт сохранённый результат вместо выполнения операции дважды.
Правила:
- Генерируйте UUID v4 на каждую логическую операцию (не на каждый сетевой ретрай — он использует тот же ключ, что и оригинал).
- Криптографически случайный источник:
random_bytes()в PHP,uuid4()в Python,randomUUID()в Node.js.uniqid()иMath.random()не подходят. - Передавайте ключ в заголовке
Idempotency-Key, длина до 255 символов.
Idempotency-Key создаст новую операцию, не вернёт кешированный результат. Если вы выполняете повтор спустя сутки, защита от дублей не работает.
Если запрос идёт повторно в окне 24 часов, ответ придёт с заголовком Idempotent-Replayed: 1 — это маркер того, что новая отправка не выполнялась.
Endpoints, поддерживающие Idempotency-Key: POST /public/v1/messages, POST /public/v1/scenarios/{id}/run, POST /public/v1/contacts (создание).
3. Подписка на webhook: подпись + защита от replay
Webhook — это HTTP-запрос от BOTIX к вашему endpoint при событии (новое сообщение, диалог завершён и т.д.). Чтобы убедиться, что запрос пришёл действительно от BOTIX, а не от подделывающего IP — проверяйте подпись.
Что приходит
POST https://your-app.example/botix-hook HTTP/1.1
Content-Type: application/json
X-Botix-Signature: a3f8c1...hex(hmac_sha256)
X-Botix-Timestamp: 1747900800
X-Botix-Event: message.received
{"event": "message.received", "data": {...}}
Алгоритм проверки
- Прочитайте сырое тело запроса (до JSON-парсинга).
- Посчитайте
hmac_sha256(raw_body, webhook_secret)в hex. - Сравните с заголовком
X-Botix-Signatureчерез constant-time функцию (защита от timing-атак). - Дополнительно: проверьте
X-Botix-Timestamp— отклонение от текущего времени не больше 5 минут (защита от replay).
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_BOTIX_SIGNATURE'] ?? '';
$timestamp = (int) ($_SERVER['HTTP_X_BOTIX_TIMESTAMP'] ?? 0);
$secret = getenv('BOTIX_WEBHOOK_SECRET');
// 1. Подпись
$expected = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
// 2. Replay window — 5 минут
if (abs(time() - $timestamp) > 300) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
// ... обработка
http_response_code(200);import hmac, hashlib, os, time
from flask import request, abort
@app.route("/botix-hook", methods=["POST"])
def botix_hook():
raw = request.get_data()
signature = request.headers.get("X-Botix-Signature", "")
timestamp = int(request.headers.get("X-Botix-Timestamp", "0"))
secret = os.environ["BOTIX_WEBHOOK_SECRET"].encode()
expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
if abs(time.time() - timestamp) > 300:
abort(401)
payload = request.get_json()
# ... обработка
return "", 200import express from "express";
import crypto from "crypto";
const app = express();
// raw body нужен ДО json-парсера
app.post("/botix-hook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("X-Botix-Signature") || "";
const timestamp = Number(req.header("X-Botix-Timestamp") || 0);
const secret = process.env.BOTIX_WEBHOOK_SECRET;
const expected = crypto
.createHmac("sha256", secret)
.update(req.body)
.digest("hex");
const sigOk = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!sigOk) return res.sendStatus(401);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(401);
const payload = JSON.parse(req.body.toString("utf8"));
// ... обработка
res.sendStatus(200);
});Idempotency обработчика
BOTIX повторяет webhook по схеме 30s / 5m / 30m / 2h / 6h, если ваш endpoint вернул не 2xx или не ответил за 10 секунд. Сохраняйте event_id из тела (поле id webhook-события) и игнорируйте повторы — иначе одно событие обработается несколько раз.
4. Пагинация
Списочные endpoints поддерживают пагинацию через query-параметры. Поля ответа лежат в meta:
GET /public/v1/contacts?page=1&per_page=50 HTTP/1.1
Authorization: Bearer btx_live_...
200 OK
{
"success": true,
"data": [ ... ],
"meta": { "page": 1, "per_page": 50, "total": 1247, "has_more": true }
}
Рекомендации:
- Не запрашивайте больше
per_page=100— превышение возвращает422. - Останавливайте обход по
has_more=false, не по достижениюtotal— между страницами могут добавиться записи. - Для большой выгрузки делайте пакеты с задержкой 100-200 мс между страницами, чтобы не упереться в
X-RateLimit-Remaining. - Используйте сортировку
?sort=-created_atпри выгрузке новых записей — стабильнее, чемidв системах с переиспользованием идентификаторов.
5. Безопасное хранение API-ключей
API-ключ btx_live_… даёт доступ к данным проекта со всеми разрешениями выбранных scopes. Утечка ≈ утечка кабинета.
Правила хранения
- Окружение, не код. Ключи — в environment-переменных (
BOTIX_API_KEY) и/или в секрет-менеджере (Vault, AWS Secrets Manager, GCP Secret Manager). Никогда не в исходниках. - Не в репозитории.
.envс реальным ключом — в.gitignore. В репо коммитьте.env.exampleс заглушкой. - Не во фронтенде. Ключ
btx_live_…— серверный. Не вставляйте его в JS-код браузера или мобильного приложения. Если интеграции нужен фронт — поднимите тонкий бэк-прокси у себя. - Минимум scopes. Делайте отдельные ключи под задачи: один для отправки сообщений (
messages:send), другой для аналитики (contacts:read,messages:read). Утечка узкого ключа — меньшая катастрофа. - Ротация при увольнении/смене подрядчика. Отзовите старый ключ в кабинете и выпустите новый. Старый ключ невозможно восстановить — это удобно для аудита.
Что делать при подозрении на утечку
- Зайдите в кабинет app.botix.pro/developers, отзовите ключ.
- Просмотрите
btx_api_logзапросов по этому ключу за последние сутки (через SA по запросу info@botix.pro) — нет ли аномалий. - Создайте новый ключ с другим именем, обновите интеграции.
- Найдите источник утечки (commit с ключом в репо, лог-файл, скриншот) и устраните.