Бот — это обычный аккаунт Эфира, которым управляет программа. Люди находят его в поиске и пишут как коллеге, а бот отвечает автоматически: присылает уведомления о заказах, отвечает на команды, просит фотоотчёты, связывает Эфир с вашими системами. Свой простой API — не Telegram, но по духу похоже.
Проще всего — прямо в приложении: Настройки → «Мои боты» → «Создать бота». Там же видно и копируется токен бота, задаются команды и аватар. Вы управляете только своими ботами (до 20 на человека). Дальше используйте токен по API, как описано ниже.
Для API-примеров ниже подставляйте токен вашего бота из «Мои боты». (Вариант для администратора: массовая веб-панель /bots/admin/ или запрос ниже под админ-токеном — управляет всеми ботами сразу.)
curl -X POST https://assortiz.ru/messenger-api/botapi/admin/create \
-H "X-Admin-Token: ВАШ_АДМИН_ТОКЕН" \
-H "Content-Type: application/json" \
-d '{"name":"Мой Бот","webhook_url":"https://ваш-сервер/hook"}'
# ответ:
# {"ok":true,"token":"ТОКЕН_БОТА","uname":"bot_xxxx","name":"Мой Бот",
# "webhook_secret":"СЕКРЕТ_ПОДПИСИ"} ← ключ проверки подписи вебхука
name — имя, под которым бот виден людям в поиске (до 64 символов).webhook_url — необязательно. Если указать, входящие будут приходить POST-ом на этот адрес (см. шаг 3б). Только https:// и только публичный адрес.token и webhook_secret из ответа. Токен — секрет бота, с ним он шлёт и принимает сообщения. webhook_secret — ключ проверки подписи вебхука (не токен). Его всегда видно и можно сменить в «Мои боты» → бот → «Секрет подписи вебхука».Самый частый сценарий — уведомления (новый заказ, статус, напоминание):
curl -X POST https://assortiz.ru/messenger-api/botapi/sendMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"to":"usrXXXX","text":"Новый заказ №123"}'
# {"ok":true,"message_id":64}
to — кому: usrXXXX (личный чат с человеком), grpXXXX (группа)
или chnXXXX (канал). Узнать usrXXXX человека можно из входящего
сообщения (поле from, шаг 3).
403 not_contacted). В группу или канал — только если владелец
добавил бота туда через приложение (иначе 403 not_member).Два способа на выбор.
Спрашиваем у Эфира новые сообщения. Параметр timeout (секунды, 0–25) —
сколько ждать ответа, если сообщений пока нет: соединение просто держится открытым и
закрывается сразу, как только сообщение появится.
curl -H "X-Bot-Token: ТОКЕН_БОТА" \
"https://assortiz.ru/messenger-api/botapi/getUpdates?offset=0&timeout=25"
# {"updates":[{"update_id":1,"from":"usrYYYY","from_name":"Иван Петров",
# "topic":"usrYYYY","seq":1,"text":"/start","cb":null,"ts":"..."}]}
Передавайте offset = последний полученный update_id, чтобы не
получать старое повторно (за раз приходит до 100 апдейтов). Отвечать — sendMessage
с to = from из апдейта (в группе — to = topic).
| Поле апдейта | Что это |
|---|---|
from, from_name | кто написал: usrXXXX и отображаемое имя |
topic | чат: usrXXXX (личка) или grpXXXX |
text | текст сообщения (у нажатой кнопки — её надпись) |
cb | данные нажатой inline-кнопки или null (см. «Кнопки») |
attachment | есть, если прислали картинку или файл: {"tp","mime","name","ref","size"} (см. «Картинки и файлы») |
type | "report_submitted" — сдан фотоотчёт (см. «Фотоотчёты») |
timeout. Без него бот, опрашивающий
сервер в цикле, шлёт запросы вхолостую: один такой давал 15 тысяч запросов в сутки —
82 % всего трафика сервера — и ни одного сообщения. С timeout=25
сообщения приходят так же быстро, а запросов в 8 раз меньше. (Если не указать — сервер
сам придержит ответ до 25 с; timeout=0 — ответить сразу.)Задайте адрес вебхука — в приложении («Мои боты» → бот → «Вебхук») или полем
webhook_url при создании. Эфир будет слать на него POST с JSON на каждое входящее
сообщение (и апдейт report_submitted о сданном фотоотчёте). Те же апдейты
параллельно лежат и в getUpdates. Тело — те же поля, что в getUpdates,
плюс bot — какой бот получил сообщение:
{"bot":"usrBOT…", ← id бота (как его видят люди), НЕ токен
"bot_uname":"bot_xxxx",
"update_id":42, "from":"usrXXXX", "from_name":"Иван",
"topic":"usrXXXX", "seq":17, "text":"привет", "cb":null,
"ts":"2026-11-08 09:15:00",
"attachment":{…}} ← только если прислали файл
Токена бота в теле и в заголовках вебхука нет: сервер-приёмник его не знает и знать не должен. Если приёмник утечёт, бота не угонят — достаточно сменить секрет подписи.
Проверяйте подпись — так вы знаете, что POST действительно от Эфира, а не от того, кто узнал ваш адрес. Каждый запрос несёт заголовки:
X-Azm-Timestamp — unix-время в секундах;X-Azm-Signature = sha256= + HMAC-SHA256 от строки
"<timestamp>." + сырое тело (байты как пришли, до разбора JSON),
ключ — webhook_secret.Где взять webhook_secret: «Мои боты» → бот → «Секрет подписи вебхука»
(кнопки «показать» и «копировать»). Он появляется, как только задан адрес вебхука.
«Сменить секрет» — старый перестаёт подходить сразу, впишите новый на своём сервере.
Админ видит секреты всех ботов в /bots/admin/.
Python (Flask):
import hmac, hashlib, time
from flask import Flask, request, abort
SECRET = "ВАШ_WEBHOOK_SECRET"
app = Flask(__name__)
def verify(headers, raw_body):
ts = headers.get("X-Azm-Timestamp", "")
got = headers.get("X-Azm-Signature", "").removeprefix("sha256=")
exp = hmac.new(SECRET.encode(), ts.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return (ts.isdigit() and abs(time.time() - int(ts)) < 300
and hmac.compare_digest(got, exp))
@app.post("/hook")
def hook():
if not verify(request.headers, request.get_data()): # сырые байты!
abort(401)
upd = request.get_json()
print(upd["from_name"], upd.get("text"))
return "ok"
Node.js (Express):
const crypto = require("crypto");
const express = require("express");
const SECRET = "ВАШ_WEBHOOK_SECRET";
const app = express();
function verify(req) {
const ts = req.get("X-Azm-Timestamp") || "";
const got = (req.get("X-Azm-Signature") || "").replace(/^sha256=/, "");
const exp = crypto.createHmac("sha256", SECRET)
.update(ts + ".").update(req.body).digest("hex"); // req.body — сырой Buffer
return /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) < 300
&& got.length === exp.length
&& crypto.timingSafeEqual(Buffer.from(got), Buffer.from(exp));
}
app.post("/hook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req)) return res.sendStatus(401);
const upd = JSON.parse(req.body);
console.log(upd.from_name, upd.text);
res.send("ok");
});
app.listen(8080);
Адрес вебхука — только https:// на публичный хост: localhost,
127.0.0.1, внутренние сети (10.*, 192.168.*…) отклоняются
и при сохранении, и перед каждой отправкой. Отвечайте быстро (до 6 с) — долгую работу
делайте после ответа.
Отвечает на любое сообщение тем же текстом. Скопируйте, подставьте токен, запустите:
import requests
BASE = "https://assortiz.ru/messenger-api/botapi"
HEAD = {"X-Bot-Token": "ТОКЕН_БОТА"}
offset = 0
while True:
# timeout=25 — ждём на стороне сервера, а не крутим пустой цикл.
# Ответ придёт сразу, как только боту напишут.
r = requests.get(f"{BASE}/getUpdates", headers=HEAD,
params={"offset": offset, "timeout": 25}, timeout=40).json()
for u in r.get("updates", []):
offset = u["update_id"]
if u.get("text"):
requests.post(f"{BASE}/sendMessage", headers=HEAD,
json={"to": u["from"], "text": "Вы написали: " + u["text"]})
Добавьте buttons — ряды кнопок. Callback (нажатие приходит боту
в getUpdates полем cb) и URL (открывает ссылку):
curl -X POST https://assortiz.ru/messenger-api/botapi/sendMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{
"to": "usrXXXX",
"text": "Подтвердить заказ №123?",
"buttons": [
[ {"text":"✅ Да","data":"ok:123"}, {"text":"❌ Нет","data":"no:123"} ],
[ {"text":"Открыть заказ","url":"https://crm.example/order/123"} ]
]
}'
Когда человек нажмёт «✅ Да», бот получит апдейт с "cb":"ok:123" и
"text":"✅ Да" — так бот понимает, какая кнопка нажата. Нажатие не оставляет
следа в переписке (как в Telegram): в ленту ничего не пишется, приходит только
cb-апдейт. Обновляйте то же меню через editMessage — чат остаётся чистым.
data — до 128.data — боту придёт надпись кнопки.url — только https:// (иначе 400 bad_button_url).Чтобы подтвердить нажатие без нового сообщения в ленте — покажите короткий тост
тому, кто нажал (аналог answerCallbackQuery). to = from из апдейта:
curl -X POST https://assortiz.ru/messenger-api/botapi/answerCallback \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"to":"usrYYYY","text":"✓ Заявка принята"}'
# заметное предупреждение вместо лёгкого тоста:
-d '{"to":"usrYYYY","text":"Заказ уже оплачен","alert":true}'
Пользователь увидит всплывающий тост (до 200 символов), лента не засоряется.
Набор кнопок под полем ввода: на телефоне открывается вместо системной клавиатуры,
на компьютере — панелью; значок у поля переключает «кнопки ↔ клавиатура». Нажатие
отправляет боту обычное сообщение с надписью кнопки — в getUpdates придёт
"text":"Остатки", без cb. Удобно для главного меню бота.
curl -X POST https://assortiz.ru/messenger-api/botapi/sendMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"to":"usrXXXX","text":"Что показать?","keyboard":{
"keyboard":[["Остатки","Мои заказы"],[{"text":"Связаться с менеджером"}]],
"resize_keyboard":true,
"one_time_keyboard":true,
"input_field_placeholder":"Выберите пункт"}}'
# убрать клавиатуру:
curl -X POST https://assortiz.ru/messenger-api/botapi/sendMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"to":"usrXXXX","text":"Готово!","remove_keyboard":true}'
| Поле | Что делает |
|---|---|
keyboard | ряды кнопок; кнопка — строка или {"text":…} |
resize_keyboard | высота по кнопкам (иначе — как у системной клавиатуры) |
one_time_keyboard | спрятать после нажатия (вернуть можно значком у поля) |
input_field_placeholder | подсказка в поле ввода, до 64 символов |
remove_keyboard | true — убрать клавиатуру |
text обязателен) и
действует, пока бот не пришлёт другую или не уберёт её.400 bad_keyboard.buttons) и клавиатуру можно прислать в одном сообщении.Каждый sendMessage возвращает message_id — по нему можно
обновлять или удалять сообщение бота. Удобно для «живого» меню: обновляете одно
сообщение вместо цепочки экранов.
# при отправке запоминаем message_id
POST .../sendMessage → {"ok":true,"message_id":64}
# отредактировать текст и кнопки того же сообщения
curl -X POST https://assortiz.ru/messenger-api/botapi/editMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"message_id":64,"text":"Обновлённое меню","buttons":[[{"text":"Дальше","data":"next"}]]}'
# ПОГАСИТЬ кнопки после нажатия (без buttons или buttons:[] — кнопок нет) —
# «Подтвердить заказ?» → после клика «✓ Заказ принят» без живых кнопок
curl -X POST https://assortiz.ru/messenger-api/botapi/editMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"message_id":64,"text":"✓ Заказ принят","buttons":[]}'
# СТАТУС-СООБЩЕНИЕ: редактируем одно и то же сообщение по мере прогресса
# «Деплой идёт…» → editMessage → «✅ Готово за 42с»
# удалить сообщение бота (у всех)
curl -X POST https://assortiz.ru/messenger-api/botapi/deleteMessage \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"message_id":64}'
Важно. editMessage заменяет сообщение целиком:
передавайте и text, и buttons — что не передали, того в новом виде
не будет. Клавиатуру, картинку и файл правкой не поменять — пришлите новое сообщение.
Отправка асинхронна (~1 сек): если вызвать edit/delete сразу после send, вернётся
409 not_yet_sent — повторите через секунду.
Бот может прислать картинку или файл по ссылке (text — необязательная подпись;
ссылка — только https://):
# картинка
-d '{"to":"usrXXXX","text":"Ваш QR-код","image":{"url":"https://.../qr.png"}}'
# файл
-d '{"to":"usrXXXX","file":{"url":"https://.../otchet.pdf","name":"Отчёт.pdf","size":204800}}'
В апдейте появится attachment с полем ref. Сами байты бот скачивает
по нему (до 25 МБ; отдаётся только файл из переписки этого бота):
# апдейт:
# {"update_id":7,"from":"usrYYYY","text":"","attachment":
# {"tp":"IM","mime":"image/jpeg","name":"photo.jpg","ref":"/v0/file/s/AbCdEf.jpg","size":181234}}
curl -H "X-Bot-Token: ТОКЕН_БОТА" -o photo.jpg \
"https://assortiz.ru/messenger-api/botapi/getFile?ref=/v0/file/s/AbCdEf.jpg"
Бот просит у сотрудника или у всей группы фотоотчёт: «сфотографируй витрину и кассу до
10:30». В чат приходит карточка «📸 Фотоотчёт» с кнопкой «Сдать отчёт»; человек снимает
фото камерой (галерея по умолчанию запрещена), при need_geo — с местом съёмки.
Бот получает фото, принимает отчёт или возвращает на пересъёмку.
curl -X POST https://assortiz.ru/messenger-api/botapi/requestReport \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"to":"usrXXXX","title":"Фото витрины","items":["витрина","касса"],
"due_at":"2026-11-09T10:30:00+03:00","min_photos":2,"need_geo":true}'
# {"ok":true,"report_id":17,"assignees":1}
| Поле | Что это |
|---|---|
to | usrXXXX (тот, кто уже писал боту) или grpXXXX (бот должен быть в группе) |
title | что снять, до 200 символов — обязательно |
items | чек-лист кадров, до 12 пунктов |
due_at | срок, ISO-время с часовым поясом; без него — бессрочно |
min_photos | сколько фото минимум, 1–20 (по умолчанию 1) |
need_geo | true — просить место съёмки |
camera_only | false — разрешить фото из галереи |
need_original | true — фото без сжатия (иначе ~2000 px) |
assignees | в группе: ["usr…"] — только этим людям (иначе всем участникам) |
Когда человек сдал отчёт, бот получает апдейт в getUpdates и на webhook
(с той же подписью; в вебхуке есть ещё поле bot):
{"update_id":9,"type":"report_submitted","report_id":17,"version":1,
"from":"usrXXXX","from_name":"Иван","topic":"usrXXXX","text":"","caption":"Витрина готова",
"photos":[{"ref":"/v0/file/s/AbCdEf.jpg","url":"/v0/file/t/…","url_exp":1794091346,
"taken_at":"2026-11-08T07:28:00+00:00","w":1600,"h":1200,"cam":1}],
"taken_at":"2026-11-08T07:28:00+00:00",
"geo":{"lat":55.75,"lon":37.61,"acc":20,"place":"Магазин №12","address":"…"}}
url — подписанная ссылка на 15 минут
(https://assortiz.ru + url, без токена), или в любой момент через
getFile?ref=….cam:1 — снято камерой; geo:{"denied":1} — человек не дал доступ к месту.version на 1 больше.Принять или вернуть на пересъёмку (uid — кто сдавал, action —
accept или return), посмотреть статусы всех адресатов:
curl -X POST https://assortiz.ru/messenger-api/botapi/reviewReport \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"report_id":17,"uid":"usrXXXX","action":"return","comment":"переснять полку"}'
curl -H "X-Bot-Token: ТОКЕН_БОТА" \
"https://assortiz.ru/messenger-api/botapi/getReport?report_id=17"
# {"report":{"id":17,"title":"Фото витрины","state":"open",
# "counts":{"waiting":0,"submitted":1,"accepted":0,"returned":0,"overdue":0},
# "people":[{"uid":"usrXXXX","name":"Иван","status":"submitted","version":1,…}]}}
Фотоотчёты включаются в пространстве отдельно; если выключены — ответ
403 disabled. Открытых запросов на бота — до 200.
Объявите команды — и при вводе «/» в чате с ботом человек увидит их списком:
curl -X POST https://assortiz.ru/messenger-api/botapi/setCommands \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"commands":[
{"command":"start","description":"Запустить бота"},
{"command":"status","description":"Статус заказов"}
]}'
До 20 команд: имя — латиница, цифры и «_» (до 32 символов), описание — до 120. Пустой список убирает меню. Команды также удобно задавать в «Мои боты» или в веб-панели. Подсказки появятся в приложении в течение ~15 секунд.
Бот может полностью стереть историю своего личного чата с человеком (у обоих):
curl -X POST https://assortiz.ru/messenger-api/botapi/clearHistory \
-H "X-Bot-Token: ТОКЕН_БОТА" \
-H "Content-Type: application/json" \
-d '{"to":"usrXXXX"}'
# {"ok":true,"deleted":44,"topic":"p2p…"}
Действие необратимо. Работает только для лички (не для групп) и только с тем, кто писал боту.
Самый простой способ постить в чат из внешней системы (CI, мониторинг, 1С) — входящий вебхук: один URL, привязанный к конкретной группе. Не нужно писать бота и знать ID чата.
# текст появится в группе от имени вашего бота (доставка ~1-3 сек)
curl -X POST https://assortiz.ru/messenger-api/hook/ХЭШ_ВЕБХУКА \
-H "Content-Type: application/json" \
-d '{"text":"✅ Деплой прошёл успешно за 42с"}'
Ключ доступа — сам неугадываемый ХЭШ_ВЕБХУКА в URL (плюс опциональный
секрет). Отдельная авторизация не нужна — удобно для скриптов. Удалить вебхук — там же в «Интеграциях».
Токен — это пароль бота: с ним можно читать входящие и писать от его имени. Передавайте
его заголовком X-Bot-Token, как во всех примерах выше:
-H "X-Bot-Token: ТОКЕН_БОТА"
Старый способ — токен прямо в адресе (/botapi/ТОКЕН/sendMessage) — по-прежнему
работает, менять существующих ботов не обязательно. Но для новых лучше заголовок:
адрес запроса попадает в журналы (свои, промежуточных прокси, браузера), а заголовки —
нет. У нас в журнале ошибок так и лежал полный токен, пока это не исправили. К тому же у
бота с токеном в заголовке свой лимит запросов, а не общий на IP.
Ошибка приходит JSON-ом {"error":"код","hint":"подсказка"} с HTTP-статусом:
| Код | Что случилось |
|---|---|
401 unknown_bot | неверный или выключенный токен |
400 text_too_long | текст длиннее 20 000 символов (не обрезается) |
400 bad_request | нет to, text, message_id и т.п. |
400 bad_buttons, bad_button_url | кнопки не того формата или больше 8×4; url не https |
400 bad_keyboard | клавиатура не того формата или больше 10×6 |
400 bad_image, bad_file | нет ссылки или она не https:// |
403 not_contacted | человек ещё не писал боту |
403 not_member | бота не добавили в группу/канал |
404 unknown_message | нет такого message_id у этого бота |
409 not_yet_sent | сообщение ещё публикуется — повторите через секунду |
429, 503 | превышен лимит запросов — подождите и повторите |
data — до 128.setCommands и clearHistory — 30 в минуту,
getFile — 120 в минуту, requestReport — 120 в час. Для активного бота при необходимости выделим отдельный.DELETE /botapi/admin/ТОКЕН_БОТА с админ-токеном.