Боты АссортиZ — Эфир

Как создать и подключить бота

Бот — это обычный аккаунт Эфира, которым управляет программа. Люди находят его в поиске и пишут как коллеге, а бот отвечает автоматически: присылает уведомления о заказах, отвечает на команды, просит фотоотчёты, связывает Эфир с вашими системами. Свой простой API — не Telegram, но по духу похоже.

Шаг 1. Получить бота

Проще всего — прямо в приложении: Настройки → «Мои боты» → «Создать бота». Там же видно и копируется токен бота, задаются команды и аватар. Вы управляете только своими ботами (до 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":"СЕКРЕТ_ПОДПИСИ"}      ← ключ проверки подписи вебхука
1name — имя, под которым бот виден людям в поиске (до 64 символов).
2webhook_url — необязательно. Если указать, входящие будут приходить POST-ом на этот адрес (см. шаг 3б). Только https:// и только публичный адрес.
3Сохраните token и webhook_secret из ответа. Токен — секрет бота, с ним он шлёт и принимает сообщения. webhook_secret — ключ проверки подписи вебхука (не токен). Его всегда видно и можно сменить в «Мои боты» → бот → «Секрет подписи вебхука».
Бот появится в поиске людей примерно через 15 секунд — шлюз создаёт его аккаунт при первом подключении.

Шаг 2. Бот отправляет сообщение

Самый частый сценарий — уведомления (новый заказ, статус, напоминание):

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).

Шаг 3. Бот принимает сообщения

Два способа на выбор.

а) Long-poll без своего сервера

Спрашиваем у Эфира новые сообщения. Параметр 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 нужен свой сервер

Задайте адрес вебхука — в приложении («Мои боты» → бот → «Вебхук») или полем 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 действительно от Эфира, а не от того, кто узнал ваш адрес. Каждый запрос несёт заголовки:

Где взять 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 с) — долгую работу делайте после ответа.

Готовый пример: эхо-бот на Python

Отвечает на любое сообщение тем же текстом. Скопируйте, подставьте токен, запустите:

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 — чат остаётся чистым.

Ответ на нажатие кнопки тост

Чтобы подтвердить нажатие без нового сообщения в ленте — покажите короткий тост тому, кто нажал (аналог 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 символов), лента не засоряется.

Клавиатура «вместо обычной» как ReplyKeyboardMarkup

Набор кнопок под полем ввода: на телефоне открывается вместо системной клавиатуры, на компьютере — панелью; значок у поля переключает «кнопки ↔ клавиатура». Нажатие отправляет боту обычное сообщение с надписью кнопки — в 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_keyboardtrue — убрать клавиатуру

Редактирование и удаление сообщений

Каждый 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}
ПолеЧто это
tousrXXXX (тот, кто уже писал боту) или grpXXXX (бот должен быть в группе)
titleчто снять, до 200 символов — обязательно
itemsчек-лист кадров, до 12 пунктов
due_atсрок, ISO-время с часовым поясом; без него — бессрочно
min_photosсколько фото минимум, 1–20 (по умолчанию 1)
need_geotrue — просить место съёмки
camera_onlyfalse — разрешить фото из галереи
need_originaltrue — фото без сжатия (иначе ~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":"…"}}

Принять или вернуть на пересъёмку (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В приложении: настройки группы → «Интеграции (вебхуки)» → «Создать вебхук». Нужен ваш бот в этой группе (он публикует сообщения от своего имени).
2Скопируйте выданный URL (кнопки «URL» и «curl») и шлите на него POST:
# текст появится в группе от имени вашего бота (доставка ~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превышен лимит запросов — подождите и повторите

Ограничения

На главную  ·  Брендбук  ·  Приложение  ·  Открыть Эфир →