v1

Best Practices

Практические рекомендации по работе с публичным API BOTIX. Код-снипеты — на PHP, Python и Node.js. Берите как образец и адаптируйте под ваш стек.

Разделы 1. Retry с экспоненциальным backoff на 429/5xx
2. Идемпотентность на стороне клиента
3. Подписка на webhook: подпись + защита от replay
4. Пагинация: cursor vs page
5. Безопасное хранение API-ключей

1. Retry с экспоненциальным backoff на 429/5xx

API возвращает 429 RATE_LIMIT_EXCEEDED при превышении лимита (с заголовком Retry-After) и 5xx при временных сбоях на нашей стороне. Корректный клиент должен повторить запрос, но не сразу — иначе попадёт в чёрный список IP по подозрению на флуд.

Алгоритм:

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 r
import { 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 вернёт сохранённый результат вместо выполнения операции дважды.

Правила:

TTL ключа — 24 часа. После истечения тот же 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": {...}}

Алгоритм проверки

  1. Прочитайте сырое тело запроса (до JSON-парсинга).
  2. Посчитайте hmac_sha256(raw_body, webhook_secret) в hex.
  3. Сравните с заголовком X-Botix-Signature через constant-time функцию (защита от timing-атак).
  4. Дополнительно: проверьте 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 "", 200
import 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 }
}

Рекомендации:

5. Безопасное хранение API-ключей

API-ключ btx_live_… даёт доступ к данным проекта со всеми разрешениями выбранных scopes. Утечка ≈ утечка кабинета.

Правила хранения

Что делать при подозрении на утечку

  1. Зайдите в кабинет app.botix.pro/developers, отзовите ключ.
  2. Просмотрите btx_api_log запросов по этому ключу за последние сутки (через SA по запросу info@botix.pro) — нет ли аномалий.
  3. Создайте новый ключ с другим именем, обновите интеграции.
  4. Найдите источник утечки (commit с ключом в репо, лог-файл, скриншот) и устраните.
API: проверяем…