SMeetBot API

SMeet Bot API

Фото и документы

Боты получают и отправляют фото и документы. Каждый файл проверяется антивирусом, прежде чем бот сможет им воспользоваться, в обе стороны, и ждать проверки можно без частого опроса.

Когда доступны файлы#

Работа с файлами включается для установки SMeet целиком, вместе с антивирусной проверкой. getMe сообщает об этом программе: "can_send_files": true.

Пока она выключена, uploadFile, getFile, скачивание, sendPhoto и sendDocument отвечают 400 INVALID_REQUEST, а сообщение с файлами приходит боту с "has_unsupported_content": true и без attachments.

Получение файла#

Файлы, которые отправил человек, перечислены в attachments сообщения:

JSON
{
  "update_id": "57",
  "event_id": "9a7c1e2b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
  "type": "message",
  "date": "2026-09-26T10:00:00Z",
  "message": {
    "message_id": "7301",
    "chat": {"id": "550", "type": "private"},
    "from": {"id": "812", "display_name": "Anna", "is_bot": false},
    "date": "2026-09-26T10:00:00Z",
    "text": "Here is the invoice",
    "attachments": [
      {"file_id": "f_Qm9vdGZpbGVfMDAwMDAx", "kind": "document", "file_name": "invoice.pdf",
       "mime_type": "application/pdf", "size": 48213, "processing_status": "scanning"}
    ]
  }
}
  • file_id служит непрозрачным идентификатором, который работает только для вашего бота.
  • kind равен photo для картинки JPEG, PNG, WebP или HEIC до 10 МиБ, voice для голосового сообщения (Голосовые сообщения), иначе document. «Фото», содержимое которого оказалось не картинкой, после проверки становится документом.
  • file_name очищено: без пути, управляющих символов и < > : " | ? *, не длиннее 120 символов. Его безопасно показывать и использовать при сохранении.
  • mime_type и size в сообщении указаны так, как их заявило приложение человека при загрузке; их ещё никто не проверял. После проверки getFile возвращает тип, определённый по содержимому, и размер сохранённой копии: опирайтесь на них. Файл не отклоняется только потому, что содержимое не совпало с заявленным типом: он хранится с определённым типом, а проверку не проходят только запрещённые типы (Как проверяются файлы).
  • processing_status обычно равен scanning. Файл больше 20 МиБ по size во вложении приходит уже в статусе rejected с "reason": "too_large" во вложении, и проверки не будет.

Голосовые сообщения#

Голосовое сообщение приходит сообщением с одним вложением вида voice и обычно без text:

JSON
"attachments": [
  {"file_id": "f_Qm9vdGZpbGVfMDAwMDAy", "kind": "voice", "file_name": "voice.ogg",
   "mime_type": "audio/ogg", "size": 18422, "processing_status": "scanning",
   "duration": 4, "transcription": "Во сколько вы открываетесь?"}
]
  • duration содержит длительность в целых секундах, как её измерило приложение человека.
  • transcription есть, только если приложение человека распознало речь. Это удобство, а не гарантия: если нужна точность, скачайте запись и распознайте её сами.
  • Запись проходит ту же проверку, что любой файл (Ожидание проверки), и скачивается так же. После проверки getFile называет звуковой тип по содержимому: запись из браузера имеет тип audio/webm, с телефона обычно audio/mp4.
  • Голосовое сообщение не считается has_unsupported_content. Видеосообщение в кружке по-прежнему считается.

Ожидание проверки#

С файлом в статусе scanning ничего не делайте. Когда проверка заканчивается, SMeet присылает событие file_status_changed с собственным event_id:

JSON
{
  "update_id": "58",
  "event_id": "2f0c6b8e-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
  "type": "file_status_changed",
  "date": "2026-09-26T10:00:05Z",
  "file_status": {"file_id": "f_Qm9vdGZpbGVfMDAwMDAx", "status": "ready", "message_id": "7301",
                  "chat": {"id": "550", "type": "private"}}
}
  • ready: вызовите getFile один раз; в ответе будет download_path.
  • rejected или scan_failed: reason объясняет причину (Коды причин). Сообщите человеку об этом короткой фразой.
  • Файл, который сканер не смог проверить (scan_failed с scanner_unavailable, scanner_outdated или scan_timeout), проверяется снова примерно через час, всего до трёх раундов. В это время getFile снова показывает scanning, а потом приходит ещё один file_status_changed.
  • file_status_changed не отправляется в чат, где человек остановил или заблокировал бота либо из которого вышел, и для удалённого сообщения.
  • Для файла, который пришёл с уже законченной проверкой, например rejected из-за размера больше 20 МиБ, file_status_changed не будет: действуйте по самому сообщению. Причина есть во вложении, в поле reason.

Так на оба события реагирует python/file_bot.py:

Pythondocs/bots/examples/python/file_bot.py
def handle(client: smeet.Client, update: dict) -> None:
    if update["type"] == "file_status_changed":
        change = update["file_status"]
        if "message_id" not in change or "chat" not in change:
            return  # the check of one of our own uploads; wait_for_file follows those with getFile
        chat_id, message_id = change["chat"]["id"], change["message_id"]
        if change["status"] == "ready":
            send_receipt(client, chat_id, message_id, change["file_id"])
        elif change["status"] in REFUSED:
            explain_refusal(client, chat_id, message_id, change["file_id"], change.get("reason"))
        return  # any other status, including ones added later, needs nothing

    if update["type"] != "message":
        return  # button presses, edits, access changes and unknown types need nothing here
    message = update["message"]
    chat_id, message_id = message["chat"]["id"], message["message_id"]
    attachments = message.get("attachments") or []
    if not attachments:
        client.send_message(chat_id, HINT, idempotency_key=smeet.action_key(update["event_id"], "hint"))
        return
    for attachment in attachments:
        file_id, status = attachment["file_id"], attachment["processing_status"]
        if status == "scanning":
            log.info("File %s is being checked; file_status_changed will say when it is done", file_id)
        elif status == "ready":
            send_receipt(client, chat_id, message_id, file_id)
        elif status in REFUSED:
            # Refused before any check (too large, for example): no file_status_changed follows, and
            # the attachment carries the reason itself.
            explain_refusal(client, chat_id, message_id, file_id, attachment.get("reason"))

Скачивание#

Shell
curl -s https://messenger.scrile.com/bot-api/v1/getFile \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"file_id": "f_Qm9vdGZpbGVfMDAwMDAx"}'
JSON
{"ok": true, "result": {"file_id": "f_Qm9vdGZpbGVfMDAwMDAx", "kind": "document", "file_name": "invoice.pdf",
 "mime_type": "application/pdf", "size": 48213, "status": "ready", "download_path": "/files/f_Qm9vdGZpbGVfMDAwMDAx/content"}}

download_path указан относительно базового адреса API, и при каждом вызове нужен токен:

Shell
curl -s -o invoice.pdf \
  https://messenger.scrile.com/bot-api/v1/files/f_Qm9vdGZpbGVfMDAwMDAx/content \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN"
  • Ответом служит сам файл, всегда как вложение (Content-Disposition: attachment с именем файла), с X-Content-Type-Options: nosniff и Cache-Control: private, no-store. Фото сохраняет тип картинки; документ сохраняет свой тип, только если это изображение, аудио, видео, PDF или обычный текст, иначе приходит как application/octet-stream.
  • Публичной или постоянной ссылки нет: права проверяются при каждом скачивании.
  • 409 FILE_NOT_READY (с retry_after), пока файл проверяется, 422 FILE_REJECTED (с reason) для отклонённого файла, 404 FILE_NOT_FOUND, когда доступ закончился (Когда заканчивается доступ).
  • Скачивание ограничено 5 запросами в секунду на бота (всплеск 10).

Загрузка файла#

uploadFile принимает multipart/form-data и Idempotency-Key:

ПолеСодержимое
fileСами байты. Обязательно и не пустое.
kindphoto или document (по умолчанию).
file_nameДо 255 символов; если его нет, берётся имя загруженной части. Имя очищается и укорачивается до 120 символов.
Shell
curl -s https://messenger.scrile.com/bot-api/v1/uploadFile \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN" \
  -H "Idempotency-Key: receipt-A-1001" \
  -F kind=document -F file_name=receipt.pdf -F file=@receipt.pdf
JSON
{"ok": true, "result": {"file_id": "f_Rm9yVGhlUmVjZWlwdDAx", "kind": "document", "file_name": "receipt.pdf",
 "mime_type": "application/pdf", "size": 20480, "status": "scanning", "retry_after": 5}}
  • Фото до 10 МиБ и по содержимому должно быть картинкой JPEG, PNG, WebP или HEIC; иначе 422 FILE_REJECTED с not_a_photo. Документ до 20 МиБ. Более крупный файл получает 413 PAYLOAD_TOO_LARGE с limit в байтах. Пустое поле file получает 400 INVALID_REQUEST с "field": "file".
  • Запрещённые типы (см. Как проверяются файлы) сразу получают 422 FILE_REJECTED с type_not_allowed.
  • Каждый бот может загрузить 200 МиБ за сутки UTC. Учитывается каждая загрузка, в том числе файлы, которые проверка потом отклонит. Сверх квоты приходит 429 QUOTA_EXCEEDED с retry_after до полуночи UTC.
  • Загрузка ограничена 2 запросами в секунду на бота (всплеск 5).
  • Загруженный файл хранится 7 дней; отправьте его в течение этого срока.
  • Тот же Idempotency-Key с теми же байтами, видом и именем возвращает тот же файл в его текущем состоянии; с любым отличием приходит 409 IDEMPOTENCY_CONFLICT.

Ожидание проверки своей загрузки#

Загрузка начинается в статусе scanning с retry_after. Спросите getFile снова ровно через столько секунд, а не в плотном цикле, пока статус не станет ready, rejected или scan_failed. Когда проверка загрузки заканчивается, SMeet тоже присылает file_status_changed, но без chat и message_id; примеры опираются на getFile и это событие пропускают.

Pythondocs/bots/examples/python/smeet.py
def wait_for_file(client: Client, file: dict, max_wait: float = 600.0) -> dict:
    """Wait until the check of a file THIS BOT UPLOADED is over; return it as getFile describes it.

    `file` is what uploadFile returned. SMeet scans every file before a bot may use it. While the
    status is "scanning" the answer carries retry_after, and we sleep exactly that long before
    asking getFile again, instead of asking in a tight loop. The result has status ready,
    rejected or scan_failed. For a file a user sent you do not need this: SMeet sends
    file_status_changed when the check of a received attachment is over (see file_bot.py).
    """
    deadline = time.monotonic() + max_wait
    info = file
    while info["status"] == "scanning":
        delay = max(1, min(int(info.get("retry_after") or 5), 60))
        if time.monotonic() + delay > deadline:
            raise TimeoutError(f"file {file['file_id']} is still {info['status']} after {max_wait:.0f} s")
        log.info("File %s is %s, asking again in %d s", file["file_id"], info["status"], delay)
        client.sleep(delay)
        info = client.get_file(file["file_id"])
    return info

Production-примеры не ждут вовсе: python/bot_logic.py бросает smeet.RetryLater(retry_after), рабочий поток откладывает это событие и тем временем отвечает в других чатах.

Отправка файла#

sendPhoto и sendDocument отправляют готовый файл обычным вложением:

JSON
{"chat_id": "550", "file_id": "f_Rm9yVGhlUmVjZWlwdDAx", "caption": "Ваша квитанция", "reply_to_message_id": "7301"}
  • file_id указывает на файл, который загрузил ваш бот, или на вложение сообщения, которое ваш бот получил в том же пространстве, пока доступ к нему сохраняется. Файл другого пространства получает 404 FILE_NOT_FOUND.
  • caption до 1024 символов; reply_to_message_id и reply_markup работают как в sendMessage.
  • Файл должен быть в статусе ready. Пока он проверяется, вызов получает 409 FILE_NOT_READY с retry_after: повторите тот же запрос с тем же ключом позже, ключ не расходуется. Отклонённый файл получает 422 FILE_REJECTED с reason.
  • sendPhoto нужен файл вида photo; любой другой файл получает 400 INVALID_REQUEST с "field": "file_id", отправьте его через sendDocument.
  • Документ всегда приходит человеку как файл для скачивания и никогда не открывается прямо в приложении.

Как проверяются файлы#

  • Каждый файл проверяется до того, как бот сможет им воспользоваться: файл от человека до того, как бот сможет его скачать, файл бота до того, как его можно отправить.
  • Байты хранятся в закрытом хранилище без публичной ссылки. Файл от человека копируется туда до проверки, и проверяется и отдаётся боту только эта копия, поэтому последующие изменения в другом месте до неё не доходят.
  • Тип определяется по содержимому, а не по имени и не по заявленному типу, и после проверки заменяет заявленный. Разметка, которую может выполнить просмотрщик (HTML, SVG, XML), скрипты, начинающиеся с #!, и программы (исполняемые файлы Windows, Linux и macOS) отклоняются, как и имена с опасными расширениями, например .html, .svg, .js, .exe, .sh, .py, .jar или .apk.
  • Антивирус (ClamAV) проверяет содержимое по сигнатурам не старше 3 дней. Чистый результат для точно такого же содержимого используется повторно, пока сигнатуры свежие.
  • Если сканер недоступен, его базы устарели или проверка не укладывается в 60 секунд, она повторяется до 3 раз с паузами 30 секунд и 2 минуты, после чего файл получает scan_failed. Примерно через час его проверяют снова, всего до трёх раундов. Без чистой проверки ни один файл не становится ready.
  • Во время технической паузы платформы проверки ждут и продолжаются после неё.

Коды причин#

reason у файла в статусе rejected или scan_failed, а также error.reason у FILE_REJECTED:

КодЗначение
malware_detectedАнтивирус нашёл вредоносное содержимое.
type_not_allowedРазметка, программа, другой запрещённый тип или опасное расширение.
not_a_photoТолько для загрузок: kind=photo, но содержимое не картинка JPEG, PNG, WebP или HEIC.
too_largeБольше допустимого: 20 МиБ для файла от человека; 10 МиБ для фото и 20 МиБ для документа, которые загружает бот. Такой файл получает rejected. Файл, который сам сканер отказался проверять из-за размера, получает scan_failed с этой причиной и повторно не проверяется.
scanner_unavailableСканер недоступен или завершился с ошибкой. Файл будет проверен позже.
scanner_outdatedСигнатуры сканера старше 3 дней. Файл будет проверен позже.
scan_timeoutПроверка не уложилась во время. Файл будет проверен позже.
source_missingФайл исчез до того, как его успели проверить.
emptyВ файле нет содержимого, например человек отправил пустой файл.

Новые коды могут появиться в любой момент. Известные превращайте в понятную фразу для человека, а для остальных используйте общую, как делает smeet.file_reason_text в примерах.

Когда заканчивается доступ#

ФайлБот может им пользоваться
Отправлен человекомПока существует сообщение, чат запущен и человек остаётся в пространстве; не дольше 30 дней.
Загружен ботом7 дней.
  • Удаление сообщения, остановка, блокировка и выход из пространства сразу прекращают доступ: getFile и скачивание отвечают 404 FILE_NOT_FOUND, и file_status_changed для таких файлов не отправляется.
  • Копию, которую ваш сервер уже скачал, SMeet отозвать не может. Удалите её по просьбе человека и напишите в описании бота, что бот хранит.

Повторы и ключи#

  • Тот же Idempotency-Key никогда не создаёт второй файл или второе сообщение.
  • FILE_NOT_READY и другие ошибки до выполнения действия ключ не расходуют: повторите тот же запрос позже.
  • К одному и тому же действию могут привести два разных события: сообщение, файл которого уже проверен, и пришедший позже file_status_changed этого файла. Стройте ключи такого действия из file_id, а не из event_id, как делает python/file_bot.py с smeet.action_key(file_id, "receipt"). Тогда повтор любого из двух событий ничего нового не отправит.

Ограничения#

ЧтоОграничение
Фото, которое загружает ботДо 10 МиБ; JPEG, PNG, WebP или HEIC по содержимому
Документ, который загружает ботДо 20 МиБ
Файл от человека, который бот может получитьДо 20 МиБ; более крупные приходят в статусе rejected с too_large
Объём загрузок200 МиБ на бота за сутки UTC (429 QUOTA_EXCEEDED)
Тело запроса21 МиБ (413 PAYLOAD_TOO_LARGE с limit)
Хранение загрузки7 дней
Доступ к полученному файлуПока это позволяют сообщение и чат, не дольше 30 дней
Загрузка и скачивание2 загрузки в секунду на бота (всплеск 5), 5 скачиваний в секунду на бота (всплеск 10)
Имя файлаОчищается и укорачивается до 120 символов