API-сообщение: вызов вашего сервера из сценария бота
API-сообщение — это шаг сценария, который вместо текста в Telegram отправляет HTTP-запрос на ваш сервер,
а ответ сервера сохраняет в переменную и передаёт в следующие сообщения цепочки. Так бот может
показать пользователю баланс из вашей CRM, номер заказа из вашей базы, результат проверки промокода и т.п.
Документ для разработчика принимающей стороны: что приходит, как ответить, как вывести ответ в боте.
1. Как это работает
пользователь → ... → [API-сообщение] → HTTP-запрос на ваш сервер
│
▼ HTTP-код ответа
┌─────────────┼──────────────┐
2xx 4xx 5xx / нет ответа
│ │ │
«успех» «ошибка клиента» «ошибка сервера»
│ │ │
▼ ▼ ▼
следующее сообщение цепочки: в тексте доступны {RESULT}, {RESULT.поле}, {RESULT_STATUS}
| Что | Как |
|---|---|
| Когда срабатывает | Когда пользователь доходит до этого шага (нажал кнопку, оплатил, пришёл по переходу) |
| Куда идёт запрос | На URL из настроек, методом GET / POST / PUT / PATCH / DELETE |
| Что определяет дальнейший путь | HTTP-код ответа: 2xx → «успех», 4xx → «ошибка клиента», всё остальное (5xx, таймаут, нет соединения) → «ошибка сервера» |
| Повторы | При сетевой ошибке или 5xx запрос повторяется столько раз, сколько указано в настройке «Повторов при ошибке» (0–5, по умолчанию 1). Ветка ошибки вызывается только если все попытки неудачны. 4xx не повторяется |
| Если ветка не задана | Цепочка завершается. Общего «следующего сообщения» у API-сообщения нет — только три ветки |
| Что получает следующее сообщение | Тело ответа, HTTP-код и ID запроса — как плейсхолдеры (раздел 5) |
2. Настройка в личном кабинете
Сообщение → тип API → вкладки:
| Вкладка | Что задаётся |
|---|---|
| Настройки | URL-адрес (только https://), формат (Content-Type), тип запроса, прокси (опционально), повторов при ошибке |
| Параметры | Три списка «ключ → значение»: Header, Query, Body. В ключах и значениях работают плейсхолдеры (раздел 3). Ключ body с точкой даёт вложенный объект: user.name → {"user": {"name": "..."}} |
| Сообщения | Три ветки: «Сообщение об успехе», «об ошибке клиента», «об ошибке сервера» |
| Тест | Выполняет запрос от вашего имени, показывает отправленное и ответ, даёт скопировать {RESULT.поле} |
| Логи | Все запросы: статус, попытки, длительность, ID запроса, отправленные данные и ответ |
Body отправляется как JSON (Content-Type: application/json, если не переопределён параметром Header).
3. Что получает ваш сервер
Системные заголовки
Добавляются всегда, если вы не задали заголовок с таким же именем в параметрах:
| Заголовок | Значение | Зачем |
|---|---|---|
X-Bott-Request-Id | UUID, например 3f1c9b2e-7d1a-4a3b-9c1e-2b6f4d8a1c55 | Уникальный ID запроса. При повторах не меняется — по нему дедуплицируйте. Он же виден в логах ЛК и доступен в боте как {RESULT_REQUEST_ID} |
X-Bott-Bot-Id | ID бота | Если один сервер обслуживает несколько ботов |
X-Bott-Message-Id | ID API-сообщения | Какой шаг сценария вызвал запрос |
X-Bott-User-Id | ID пользователя в Bott | Внутренний ID (не Telegram ID). Telegram ID передайте параметром {USER_TELEGRAM_ID} |
User-Agent | Bott-MessageApi/1.0 | |
Content-Type | из настройки «Формат» |
Плейсхолдеры для подстановки в URL, заголовки, query и body
| Плейсхолдер | Значение |
|---|---|
{USER_TELEGRAM_ID} | Telegram ID пользователя |
{USER_ID} | ID пользователя в боте |
{USERNAME} | @username |
{FIRST_NAME}, {SECOND_NAME}, {NAME} | Имя, фамилия, имя-ссылка |
{BALANCE}, {BALANCE_WITHOUT_CURRENCY} | Баланс пользователя в боте |
{ATTR_ключ} | Значение атрибута пользователя (например {ATTR_city}) |
{USER_ATTRIBUTES} | Все атрибуты списком |
{ORDER_ID}, {ORDER_KEY} | Заказ, если шаг вызван после оплаты |
{BOT_ID}, {BOT_NAME}, {BOT_TITLE} | Бот |
{MESSAGE_ID}, {MESSAGE_TITLE} | Текущее API-сообщение |
{CURRENT_DATE}, {CURRENT_TIME}, {CURRENT_DATETIME} | Время в часовом поясе бота |
{RESULT}, {RESULT.поле}, {RESULT_STATUS} | Ответ предыдущего API-сообщения, если в цепочке их несколько |
Пример фактического запроса
Настройки: POST https://api.example.com/bott/balance, Header Authorization: Bearer SECRET,
Query source=bot, Body telegram_id={USER_TELEGRAM_ID}, user.name={FIRST_NAME}.
curl -X POST 'https://api.example.com/bott/balance?source=bot' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer SECRET' \
-H 'User-Agent: Bott-MessageApi/1.0' \
-H 'X-Bott-Request-Id: 3f1c9b2e-7d1a-4a3b-9c1e-2b6f4d8a1c55' \
-H 'X-Bott-Bot-Id: 42' \
-H 'X-Bott-Message-Id: 1017' \
-H 'X-Bott-User-Id: 5581' \
-d '{"telegram_id":"777000000","user":{"name":"Alex"}}'
4. Как сформировать ответ
Это главный раздел: от формы ответа зависит, что сможет показать бот.
Правила
- Отдавайте JSON с
Content-Type: application/json. Каждое поле станет плейсхолдером{RESULT.поле}; вложенность — через точку:{RESULT.data.user.name}. Элементы массива — по индексу:{RESULT.items.0.title}. - HTTP-код = ветка сценария:
200— всё хорошо → «сообщение об успехе»;4xx(например404 пользователь не найден,400 промокод недействителен) → «сообщение об ошибке клиента». Не повторяется;5xx— сбой у вас → Bott повторит запрос, затем «сообщение об ошибке сервера».
- Идемпотентность. Повтор приходит с тем же
X-Bott-Request-Id. Если действие не должно выполняться дважды (списание, создание заказа) — запомните ID и на повтор отдайте тот же ответ. - Размер. В переменную попадают первые 60 000 символов тела; в лог ЛК — первые 8 KB. Не возвращайте HTML, файлы, большие списки.
- Время. 5 секунд на соединение, 20 секунд на ответ. Долгие операции выносите в фон и отвечайте сразу.
- Текст для пользователя удобно отдавать готовым полем — тогда в боте достаточно
{RESULT.message}, а логику формулировок вы держите у себя.
Рекомендуемая форма
Успех (200):
{
"ok": true,
"message": "Ваш баланс: 1 250 ₽",
"data": {
"balance": 1250,
"currency": "RUB",
"level": "gold",
"orders": [
{ "id": 501, "status": "paid" },
{ "id": 502, "status": "new" }
]
}
}
Ошибка клиента (404):
{
"ok": false,
"error": "Пользователь не найден. Привяжите аккаунт на сайте",
"code": "user_not_found"
}
Сбой сервера (500) — тело любое, Bott повторит запрос.
Если ответ не JSON
Тело целиком доступно как {RESULT}, а все {RESULT.поле} заменяются пустой строкой. Это допустимо для простых
случаев («ОК» / «Готово»), но лишает гибкости.
5. Как передать ответ в следующие сообщения Bott
Плейсхолдеры ответа
| Плейсхолдер | Что подставляется | Пример для ответа выше |
|---|---|---|
{RESULT} | Тело ответа целиком | {"ok":true,"message":...} |
{RESULT.путь} | Поле JSON по пути через точку | {RESULT.data.balance} → 1250, {RESULT.data.orders.0.id} → 501 |
{RESULT_STATUS} | HTTP-код ответа | 200 |
{RESULT_REQUEST_ID} | ID запроса (X-Bott-Request-Id) | 3f1c9b2e-… |
Значения: строки и числа — как есть; true/false — словами; объект или массив — JSON-строкой;
отсутствующее поле или null — пустая строка.
Где работают
- В тексте и в кнопках (подпись и URL) любого сообщения, идущего после API-сообщения в цепочке:
сообщение ветки → его «следующее сообщение» → и далее, пока цепочка не закончится. - Переменная живёт в контексте одной отправки: если дальше в цепочке стоит другое API-сообщение,
оно перезапишет{RESULT}своим ответом. - Цепочка прерывается на платёжном сообщении.
Пошагово
- Создайте API-сообщение, укажите URL, метод, параметры (
telegram_id={USER_TELEGRAM_ID}в body). - Откройте вкладку Тест → «Выполнить тестовый запрос». Увидите, что ушло на сервер и что он ответил.
- В дереве ответа нажмите на нужное поле — плейсхолдер (например
{RESULT.data.balance}) скопируется. - Создайте текстовое сообщение «Ваш баланс: {RESULT.data.balance} {RESULT.data.currency}. Уровень: {RESULT.data.level}» и прикрепите его на вкладке Сообщения как «Сообщение об успехе».
- Для ветки «ошибка клиента» создайте сообщение с текстом
{RESULT.error}— сервер сам объяснит пользователю, что не так. - К сообщению об успехе добавьте «следующее сообщение» с кнопкой «Мои заказы» и URL
https://example.com/orders?user={USER_TELEGRAM_ID}&rid={RESULT_REQUEST_ID}— плейсхолдеры работают и там.
Что увидит пользователь при ответе из раздела 4:
Ваш баланс: 1250 RUB. Уровень: gold
6. Примеры приёмного сервера
Все три примера реализуют один и тот же endpoint POST /bott/balance:
- читают
X-Bott-Request-Idи Telegram ID из body; - дедуплицируют повторы по ID запроса;
- отдают
200с данными,404с текстом ошибки или500при сбое.
Рядом — тексты сообщений Bott, которые этот ответ выводят:
| Ветка | Текст сообщения в Bott |
|---|---|
| Успех | {RESULT.message} Заказов: {RESULT.data.orders_count}, последний — №{RESULT.data.last_order.id} |
| Ошибка клиента | ⚠️ {RESULT.error} |
| Ошибка сервера | Сервис временно недоступен, попробуйте позже (запрос {RESULT_REQUEST_ID}) |
Python (Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = {"777000000": {"balance": 1250, "currency": "RUB", "orders": [{"id": 501}, {"id": 502}]}}
SEEN_REQUESTS: dict[str, tuple[dict, int]] = {} # request_id -> (json, status); в бою — Redis/БД
@app.post("/bott/balance")
def balance():
request_id = request.headers.get("X-Bott-Request-Id", "")
if request_id in SEEN_REQUESTS: # повтор от Bott — отдаём тот же ответ
body, status = SEEN_REQUESTS[request_id]
return jsonify(body), status
payload = request.get_json(silent=True) or {}
telegram_id = str(payload.get("telegram_id", ""))
user = USERS.get(telegram_id)
if user is None: # 4xx -> ветка «ошибка клиента», без повторов
body, status = {"ok": False, "error": "Аккаунт не найден. Привяжите Telegram на сайте", "code": "user_not_found"}, 404
else: # 200 -> ветка «успех»
body, status = {
"ok": True,
"message": f"Ваш баланс: {user['balance']} {user['currency']}",
"data": {
"balance": user["balance"],
"currency": user["currency"],
"orders_count": len(user["orders"]),
"last_order": user["orders"][-1] if user["orders"] else None,
},
}, 200
SEEN_REQUESTS[request_id] = (body, status)
return jsonify(body), status
# Любое необработанное исключение даст 500 -> Bott повторит запрос, затем ветка «ошибка сервера»
PHP (без фреймворка)
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
$requestId = $_SERVER['HTTP_X_BOTT_REQUEST_ID'] ?? '';
$payload = json_decode(file_get_contents('php://input'), true) ?: [];
$telegramId = (string)($payload['telegram_id'] ?? '');
$users = ['777000000' => ['balance' => 1250, 'currency' => 'RUB', 'orders' => [['id' => 501], ['id' => 502]]]];
// Дедупликация повторов: в бою — таблица/кэш с TTL, здесь файл для наглядности
$cacheFile = sys_get_temp_dir() . '/bott_' . preg_replace('/[^a-f0-9\-]/', '', $requestId) . '.json';
if ($requestId !== '' && is_file($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
http_response_code($cached['status']);
echo json_encode($cached['body'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
if (!isset($users[$telegramId])) {
$status = 404; // ветка «ошибка клиента»
$body = ['ok' => false, 'error' => 'Аккаунт не найден. Привяжите Telegram на сайте', 'code' => 'user_not_found'];
} else {
$user = $users[$telegramId];
$status = 200; // ветка «успех»
$body = [
'ok' => true,
'message' => sprintf('Ваш баланс: %d %s', $user['balance'], $user['currency']),
'data' => [
'balance' => $user['balance'],
'currency' => $user['currency'],
'orders_count' => count($user['orders']),
'last_order' => $user['orders'] ? end($user['orders']) : null,
],
];
}
} catch (Throwable $e) {
$status = 500; // ветка «ошибка сервера», Bott повторит запрос
$body = ['ok' => false, 'error' => 'internal'];
}
if ($requestId !== '' && $status !== 500) {
file_put_contents($cacheFile, json_encode(['status' => $status, 'body' => $body]));
}
http_response_code($status);
echo json_encode($body, JSON_UNESCAPED_UNICODE);
JavaScript (Node.js + Express)
import express from 'express';
const app = express();
app.use(express.json());
const users = { '777000000': { balance: 1250, currency: 'RUB', orders: [{ id: 501 }, { id: 502 }] } };
const seenRequests = new Map(); // requestId -> { status, body }; в бою — Redis с TTL
app.post('/bott/balance', (req, res) => {
const requestId = req.get('X-Bott-Request-Id') || '';
if (seenRequests.has(requestId)) {
const { status, body } = seenRequests.get(requestId); // повтор от Bott
return res.status(status).json(body);
}
const telegramId = String(req.body?.telegram_id ?? '');
const user = users[telegramId];
let status;
let body;
if (!user) {
status = 404; // ветка «ошибка клиента», без повторов
body = { ok: false, error: 'Аккаунт не найден. Привяжите Telegram на сайте', code: 'user_not_found' };
} else {
status = 200; // ветка «успех»
body = {
ok: true,
message: `Ваш баланс: ${user.balance} ${user.currency}`,
data: {
balance: user.balance,
currency: user.currency,
orders_count: user.orders.length,
last_order: user.orders.at(-1) ?? null,
},
};
}
seenRequests.set(requestId, { status, body });
res.status(status).json(body);
});
// Необработанная ошибка -> 500 -> Bott повторит запрос, затем ветка «ошибка сервера»
app.use((err, req, res, next) => res.status(500).json({ ok: false, error: 'internal' }));
app.listen(3000);
7. Ограничения
| Ограничение | Значение |
|---|---|
| Протокол | Только https://. Адреса localhost, приватные и служебные IP отклоняются |
| Таймауты | 5 с на соединение, 20 с на ответ |
| Редиректы | Не выполняются: 301/302 попадут в ветку «ошибка сервера» — отдавайте конечный URL |
| Повторы | Только при сетевой ошибке и 5xx; пауза 0,5 с × номер попытки; максимум 5 повторов |
| Тело ответа | В переменную — до 60 000 символов, в лог — до 8 KB |
| Дедупликация Bott | Один запрос на платёж (payment_history_id) в течение 5 минут |
| Заголовки-секреты в логах | Authorization, *-Key, *-Token, Cookie и т.п. маскируются |
8. Диагностика
Вкладка Логи в настройках API-сообщения. Каждая запись:
- статус (
HTTP 200,HTTP 404,нет ответа), «тест» для тестовых запросов; - «попытка N из M» и длительность, если были повторы;
- ID запроса с копированием — сопоставьте с логами вашего сервера по
X-Bott-Request-Id; - «Подробнее»: адрес, заголовки (с маской), query, тело запроса, тело ответа, причина ошибки.
Типовые ситуации:
| В логе | Причина | Что делать |
|---|---|---|
адрес или прокси не прошли проверку безопасности | http://, локальный/приватный адрес | Использовать публичный https:// |
не удалось подключиться к серверу | DNS, закрытый порт, невалидный TLS-сертификат | Проверить доступность извне и сертификат |
превышено время ожидания ответа | Ответ дольше 20 с | Ускорить обработку или отвечать сразу, работу делать в фоне |
HTTP 301/302 | Редирект | Указать конечный URL |
HTTP 415/400 от вашего сервера | Не принят JSON | Проверить Content-Type и разбор тела |
{RESULT.поле} пустой в боте | Ответ не JSON или путь неверный | Сверить путь по дереву на вкладке «Тест» |