Перейти к содержанию

Чаты

API для работы с чатами LocalHub позволяет получить список доступных аккаунту групповых чатов, подписать робота на конкретный чат и получать новые сообщения и события через механизм обновлений.

Что важно знать

  • Все методы требуют scope chats.read.
  • Сейчас поддерживаются только групповые чаты.
  • Робот видит чат и может быть подписан на него, если владелец-аккаунт либо имеет право добавлять администраторов в этот чат, либо является создателем чата. В остальных случаях чат недоступен (404 chat_not_found).
  • Один групповой чат может быть подключён только к одному роботу. Чтобы сменить робота, снимите существующую подписку через DELETE /v1/chats/{chat_id}/subscribe, а затем подпишите другого робота.
  • Подписка действует с момента создания: сообщения, отправленные в чат до подписки, в обновления не попадают.
  • Клиент подтверждает обработанные обновления параметром offset. Подтверждённые обновления повторно не возвращаются.
  • Неподтверждённые обновления доступны ограниченное время. Клиент должен регулярно получать обновления и сохранять последний обработанный update_id.
  • При длительном отсутствии активности подписки робота могут быть сняты автоматически. Проверить активные подписки можно через GET /v1/chats/subscriptions; чтобы возобновить работу, оформите подписки заново.
  • В обновлениях передаётся только текстовая часть сообщений. Вложения (медиа, файлы, стикеры, голосовые) не возвращаются. Если у сообщения есть и текст, и вложение, робот получит только message.text. Сообщения без текстовой части в обновления не попадают.
  • Два режима получения обновлений — long polling (POST /v1/chats/updates) и webhook-push (Robot выполняет POST на ваш HTTPS URL). Режимы взаимоисключающие: пока для робота настроен webhook, POST /v1/chats/updates возвращает 409 webhook_set. Чтобы вернуться к long polling, выполните DELETE /v1/chats/webhook.

Модель доступа к чатам

Подписка робота — это делегирование части прав владельца аккаунта, а не самостоятельное членство робота в чате. Робот получает доступ только к тем групповым чатам, для которых у аккаунта-владельца в этом чате есть право добавлять администраторов или является создателем чата.

Если аккаунт теряет доступ к чату, чат перестаёт быть доступен роботу. Активная подписка может быть снята, а новые обновления по этому чату перестанут возвращаться. После восстановления доступа подпишите робота на чат заново.

Список методов

Метод Путь Назначение
GET /v1/chats список чатов аккаунта
GET /v1/chats/{chat_id} информация по чату
GET /v1/chats/{chat_id}/members список участников чата
POST /v1/chats/{chat_id}/subscribe подписаться на чат
DELETE /v1/chats/{chat_id}/subscribe отписаться от чата
GET /v1/chats/subscriptions список активных подписок
POST /v1/chats/{chat_id}/invites создать invite-ссылку
POST /v1/chats/updates получить новые обновления
PUT /v1/chats/webhook установить или обновить webhook
GET /v1/chats/webhook получить конфигурацию webhook'а
DELETE /v1/chats/webhook снять webhook

GET /v1/chats — список чатов

Возвращает список чатов аккаунта-владельца, к которым у робота есть доступ. В текущей версии возвращаются только групповые чаты.

Чат попадает в выдачу, только если владелец-аккаунт либо имеет право добавлять администраторов в этот чат, либо является создателем чата. В остальных случаях чат не возвращается.

Scope: chats.read

Response 200:

{
  "chats": [
    {
      "id": "019560a1-aaaa-7def-8901-234567890abc",
      "type": "group",
      "name": "Рабочая группа",
      "permission": "ro"
    }
  ]
}
Поле Тип Описание
chats[].id UUID идентификатор чата
chats[].type string тип чата, в текущей версии group
chats[].name string название чата
chats[].permission string права робота на этот чат: ro — чтение, rw — чтение и запись

GET /v1/chats/{chat_id} — информация по чату

Возвращает поля чата из GET /v1/chats плюс число участников, дату создания, описание, создателя и последнее сообщение.

Scope: chats.read

Path: chat_id — UUID чата из GET /v1/chats.

Response 200:

{
  "id": "019560a1-aaaa-7def-8901-234567890abc",
  "type": "group",
  "name": "Рабочая группа",
  "permission": "ro",
  "description": "Канал команды разработки",
  "created_at": "2026-01-15T09:00:00Z",
  "members_count": 42,
  "owner": {
    "id": "019560a1-1111-7def-8901-234567890abc",
    "login": "alex",
    "name": "Алексей"
  },
  "last_message": {
    "id": "019560a1-eeee-7def-8901-234567890abc",
    "sender": {
      "id": "019560a1-ffff-7def-8901-234567890abc",
      "login": "maria",
      "name": "Мария"
    },
    "text": "Всем привет!",
    "sent_at": "2026-04-28T10:00:00Z"
  }
}
Поле Тип Описание
id UUID идентификатор чата
type string тип чата (group)
name string название чата
permission string права робота на этот чат (ro / rw)
description string | null описание чата
created_at datetime дата создания чата (UTC, ISO 8601)
members_count int число участников чата
owner object создатель чата
owner.id UUID идентификатор пользователя
owner.login string логин
owner.name string отображаемое имя
last_message object | null последнее сообщение чата или null, если сообщений нет
last_message.id UUID идентификатор сообщения
last_message.sender object отправитель (id, login, name — как в owner)
last_message.text string текст сообщения
last_message.sent_at datetime время отправки (UTC)

Response 404 chat_not_found — чат недоступен аккаунту, не поддерживается текущей версией API, либо владелец-аккаунт не является создателем чата и не имеет права добавлять администраторов в этот чат.

GET /v1/chats/{chat_id}/members — список участников

Возвращает участников указанного чата.

Scope: chats.read

Path: chat_id — UUID чата из GET /v1/chats.

Response 200:

{
  "members": [
    {
      "id": "019560a1-dddd-7def-8901-234567890abc",
      "login": "maria",
      "name": "Мария",
      "last_online": "2026-04-28T09:30:00Z"
    },
    {
      "id": "019560a1-eeee-7def-8901-234567890abc",
      "login": "alex",
      "name": "Алексей",
      "last_online": null
    }
  ]
}
Поле Тип Описание
members[].id UUID идентификатор пользователя
members[].login string логин
members[].name string отображаемое имя; если имя не задано, может совпадать с login
members[].last_online datetime | null время последнего онлайна (UTC, ISO 8601) или null, если неизвестно

Порядок участников в ответе не специфицирован.

Response 404 chat_not_found — чат недоступен аккаунту, не поддерживается текущей версией API, либо владелец-аккаунт не является создателем чата и не имеет права добавлять администраторов в этот чат.

POST /v1/chats/{chat_id}/subscribe — подписаться

Создаёт подписку робота на обновления чата. После этого новые сообщения и события чата доступны через POST /v1/chats/updates.

Scope: chats.read

Path: chat_id — UUID чата, полученный из GET /v1/chats.

Body: не требуется.

Response 200:

{
  "id": "019dce22-1111-7def-8901-234567890abc",
  "created_at": "2026-04-28T10:00:00Z",
  "chat": {
    "id": "019560a1-aaaa-7def-8901-234567890abc",
    "type": "group",
    "name": "Рабочая группа",
    "permission": "ro"
  }
}
Поле Тип Описание
id UUID идентификатор подписки
created_at datetime момент создания подписки (UTC, ISO 8601)
chat object те же поля, что отдаёт GET /v1/chats
chat.id UUID идентификатор чата
chat.type string тип чата (group)
chat.name string название чата
chat.permission string права робота (ro / rw)

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

Если подписка на этот чат уже принадлежит другому роботу, в том числе из другого аккаунта, API возвращает 409 chat_already_subscribed.

Response 404 chat_not_found — чат недоступен аккаунту, не поддерживается текущей версией API, либо владелец-аккаунт не является создателем чата и не имеет права добавлять администраторов в этот чат.

Response 409 chat_already_subscribed — чат уже подписан другим роботом. Снимите существующую подписку и повторите запрос.

DELETE /v1/chats/{chat_id}/subscribe — отписаться

Удаляет подписку робота на чат. Если подписки нет, запрос всё равно завершается успешно.

После отписки неподтверждённые обновления по этому чату больше не возвращаются роботу. Последующая повторная подписка начнёт получение новых обновлений с момента её создания.

Scope: chats.read

Path: chat_id — UUID чата.

Response 204 — запрос выполнен успешно, тело ответа пустое.

GET /v1/chats/subscriptions — список подписок

Возвращает все активные подписки текущего робота.

Scope: chats.read

Response 200:

{
  "subscriptions": [
    {
      "id": "019dce22-1111-7def-8901-234567890abc",
      "created_at": "2026-04-28T10:00:00Z",
      "chat": {
        "id": "019560a1-aaaa-7def-8901-234567890abc",
        "type": "group",
        "name": "Рабочая группа",
        "permission": "ro"
      }
    }
  ]
}

Каждая подписка содержит вложенный chat с тем же набором полей, что и в POST /v1/chats/{chat_id}/subscribe и GET /v1/chats. Подписки на чаты, к которым доступ владельца потерян, в выдачу не попадают.

POST /v1/chats/{chat_id}/invites — создать invite-ссылку

Создаёт персональную одноразовую ссылку на вступление в чат. Срок действия ссылки — 24 часа.

Опциональное поле external_user_id сохраняется вместе со ссылкой и возвращается в событии member_joined, когда пользователь по ней вступит в чат. Используется для сопоставления вступившего LocalHub-пользователя с учётной записью во внешней системе.

Требования к получателю ссылки

Для перехода по ссылке у пользователя должно быть установлено мобильное приложение социальной сети LocalHub. Если приложение не установлено, его необходимо установить и войти в существующий аккаунт LocalHub либо зарегистрироваться. Только после этого переход по ссылке осуществит переход для вступления в чат.

Scope: chats.read

Path: chat_id — UUID чата из GET /v1/chats.

Body (JSON):

{
  "external_user_id": "user-12345"
}
Поле Тип Описание
external_user_id string | null Опциональное. 1–128 символов. Если не передано — поле не появится в последующем событии member_joined.

Response 200:

{
  "invite_url": "https://local-hub.ru/chat-invite/Z3kVxyT9pBnQ4mLrE7sJfH2aD8gWcN5UvP6oR1XbCYq",
  "expires_at": "2026-05-03T10:00:00Z"
}
Поле Тип Описание
invite_url string URL для вступления в чат. Передайте его пользователю.
expires_at datetime Срок действия ссылки в UTC.

Ошибки:

  • 400 validation_errorexternal_user_id пустая строка или длиннее 128 символов.
  • 403 permission_denied — у робота нет scope chats.read.
  • 404 chat_not_found — чат не существует или недоступен роботу.
  • 503 service_unavailable — сервис временно недоступен. Повторите запрос позже.

Событие member_joined со связкой

Когда пользователь переходит по ссылке и вступает в чат, в потоке обновлений (long polling или webhook) появляется событие member_joined с полем external_user_id, заданным при создании ссылки:

{
  "update_id": 142,
  "chat_id": "019560a1-aaaa-7def-8901-234567890abc",
  "type": "member_joined",
  "received_at": "2026-05-02T12:30:00Z",
  "member": {
    "id": "019560a1-dddd-7def-8901-234567890abc",
    "login": "maria",
    "name": "Мария"
  },
  "external_user_id": "user-12345"
}

Поле external_user_id доставляется один раз — для события вступления по этой ссылке. В других событиях member_joined оно равно null, если ссылка не была создана с external_user_id.

POST /v1/chats/updates — получить обновления

Основной метод для чтения сообщений и событий чатов.

Scope: chats.read

Параметры

Передаются в query-string.

Параметр Тип Значение по умолчанию Описание
offset int null Вернуть обновления с update_id >= offset и подтвердить все обновления с update_id < offset. null или 0 — получить доступные неподтверждённые обновления без подтверждения предыдущих.
limit int 100 Максимум обновлений за один ответ. Допустимо 1–100.
chat_id UUID отсутствует Необязательный фильтр по конкретному чату. Подтверждение через offset применяется ко всем обновлениям робота, включая обновления из других чатов.
timeout int 0 Long polling. 0 (по умолчанию) — ответ возвращается немедленно. 130 — если доступных обновлений нет, сервер удерживает соединение до появления нового обновления для этого робота или до истечения timeout секунд, после чего возвращает результат (возможно пустой). Значения вне [0, 30]422.

Response 409 webhook_set — для робота настроен webhook. Long polling и webhook-доставка взаимоисключающие. Чтобы воспользоваться long polling, снимите webhook через DELETE /v1/chats/webhook.

Response 200

{
  "updates": [
    {
      "update_id": 42,
      "chat_id": "019560a1-aaaa-7def-8901-234567890abc",
      "type": "message",
      "received_at": "2026-04-28T12:05:01Z",
      "message": {
        "id": "019560a1-eeee-7def-8901-234567890abc",
        "sender": {
          "id": "019560a1-ffff-7def-8901-234567890abc",
          "login": "alex",
          "name": "Алексей"
        },
        "text": "Привет!",
        "sent_at": "2026-04-28T12:05:00Z"
      }
    },
    {
      "update_id": 43,
      "chat_id": "019560a1-aaaa-7def-8901-234567890abc",
      "type": "member_joined",
      "received_at": "2026-04-28T12:06:00Z",
      "member": {
        "id": "019560a1-dddd-7def-8901-234567890abc",
        "login": "maria",
        "name": "Мария"
      }
    }
  ]
}

Структура обновления

Поле Тип Описание
update_id int монотонно возрастающий идентификатор обновления, уникальный для робота
chat_id UUID идентификатор чата-источника
type string тип события: message, member_joined, member_left
received_at datetime время получения обновления сервисом (UTC)
message object | null объект сообщения; не-null только при type=message, для остальных типов — null. Содержит вложенный объект sender.
member object | null участник JOIN/LEAVE-события; не-null при member_joined и member_left, для messagenull. Это сам User-объект {id, login, name} (без вложенного user).
external_user_id string | null Только для member_joined. Заполнено, если пользователь вступил по invite-ссылке, созданной через POST /v1/chats/{chat_id}/invites с указанным external_user_id.

Типы обновлений

message — новое сообщение

Поле message:

Поле Тип Описание
id UUID идентификатор сообщения
sender object отправитель сообщения
text string текст сообщения. Вложения не передаются (см. «Что важно знать»).
sent_at datetime время отправки сообщения (UTC)

member_joined — участник вступил в чат

member_left — участник покинул чат

Для обоих типов поле member — это сам User-объект (без обёртки).

Объект user / sender / member

Поля message.sender и member имеют одинаковую структуру:

Поле Тип Описание
id UUID UUID пользователя в LocalHub
login string стабильный обязательный логин
name string отображаемое имя; если имя не задано, может совпадать с login

Как получать обновления

offset одновременно задаёт нижнюю границу выдачи и подтверждает ранее обработанные обновления.

  1. Первый запрос выполните без offset или с offset=0.
  2. Обработайте полученные обновления и сохраните максимальный update_id.
  3. Следующий запрос выполните с offset = max(update_id) + 1.
  4. Повторяйте цикл с задержкой между запросами.

Подтверждённое обновление не возвращается повторно

После запроса с offset=N обновления с update_id < N считаются обработанными. Сохраняйте необходимые данные до сдвига offset.

Повторные и параллельные запросы

Запросы с одним и тем же offset возвращают одинаковый набор обновлений. Это безопасно для retry. При параллельных запросах клиент должен дедуплицировать update_id.

Рекомендованный режим: long polling

Параметр timeout (от 1 до 30 секунд) удерживает соединение до появления нового обновления для этого робота или до истечения таймаута. Это рекомендованный режим работы для постоянных интеграций: минимальная задержка доставки сообщений, минимум пустых запросов, минимальная нагрузка на rate-limit.

#!/bin/bash
: "${ROBOT_API_URL:?need ROBOT_API_URL}"
: "${ROBOT_TOKEN:?need ROBOT_TOKEN}"
URL='https://robot.prod.lclhub.ru/v1/chats/updates'
OFFSET=0

while true; do
  RESP=$(curl -fsS -X POST \
    "$URL?offset=$OFFSET&limit=100&timeout=25" \
    -H "Authorization: Bearer $ROBOT_TOKEN" \
    --max-time 35)
  COUNT=$(echo "$RESP" | jq '.updates | length')
  if [ "$COUNT" -gt 0 ]; then
    echo "$RESP" | jq -c '.updates[]'
    MAX=$(echo "$RESP" | jq '[.updates[].update_id] | max')
    OFFSET=$((MAX + 1))
  fi
done
import os

import httpx

BASE_URL = os.environ["ROBOT_API_URL"].rstrip("/")
TOKEN = os.environ["ROBOT_TOKEN"]
URL = f"{BASE_URL}/chats/updates"

def main() -> None:
    offset = 0
    with httpx.Client(timeout=35) as client:
        while True:
            response = client.post(
                URL,
                params={"offset": offset, "limit": 100, "timeout": 25},
                headers={"Authorization": f"Bearer {TOKEN}"},
            )
            response.raise_for_status()
            updates = response.json()["updates"]
            if updates:
                for update in updates:
                    handle(update)
                offset = max(item["update_id"] for item in updates) + 1

def handle(update: dict) -> None:
    if update["type"] == "message":
        message = update["message"]
        print(f"[{update['chat_id']}] {message['sender']['name']}: {message['text']}")
    elif update["type"] == "member_joined":
        print(f"+ {update['member']['name']} вступил в {update['chat_id']}")
    elif update["type"] == "member_left":
        print(f"- {update['member']['name']} вышел из {update['chat_id']}")

if __name__ == "__main__":
    main()

HTTP-таймаут клиента — больше серверного timeout

Long-poll-запрос может висеть до timeout секунд. Ставьте таймаут HTTP-клиента с запасом 5–10 секунд (--max-time 35, httpx.Client(timeout=35)), иначе клиентская библиотека оборвёт запрос до того, как сервер успеет вернуть ответ.

Подтверждение работает идентично

offset подтверждает обработанные обновления до ожидания, а не после. Поведение offset и limit не зависит от timeout.

Один long-poll-цикл на робота

Параллельные long-poll-запросы одного и того же робота будут получать одинаковые обновления (retry-safe), но впустую расходуют соединения и rate-limit. Поддерживайте ровно один активный цикл.

При потере состояния клиента

Если клиент не сохранил последний offset, выполните запрос без offset. API вернёт доступные неподтверждённые обновления. После их обработки сохраните максимальный update_id и продолжайте цикл с offset = max(update_id) + 1.

Short polling — для разовых проверок

Запрос без timeout (или с timeout=0) возвращает ответ немедленно с доступными обновлениями. Этот режим не подходит для непрерывного получения событий:

  • задержка доставки сообщения ≈ половина poll-интервала (типично 2–3 с);
  • большинство запросов — пустые и быстро съедают rate-limit;
  • нет преимуществ перед long polling в любом сценарии.

Используйте short polling для разовых проверок, где не нужен непрерывный цикл:

# Разовая проверка без цикла:
curl -fsS -X POST "${ROBOT_API_URL%/}/chats/updates?limit=10" \
  -H "Authorization: Bearer $ROBOT_TOKEN" | jq
# Один запрос без цикла:
import os
import httpx

base_url = os.environ["ROBOT_API_URL"].rstrip("/")
response = httpx.post(
    f"{base_url}/chats/updates",
    params={"limit": 10},
    headers={"Authorization": f"Bearer {os.environ['ROBOT_TOKEN']}"},
    timeout=10,
)
print(response.json())

Для непрерывного получения сообщений используйте long polling из раздела выше.

Webhook — push-доставка обновлений

Альтернатива long polling: Robot выполняет POST на ваш HTTPS URL по мере появления обновлений в подписанных чатах. Один запрос = один update.

На одного робота можно настроить один webhook. Пока webhook настроен (в любом статусе — active или paused), POST /v1/chats/updates возвращает 409 webhook_set.

PUT /v1/chats/webhook — установить или обновить

Регистрирует webhook робота. Идемпотентен: повторный PUT поверх существующего active-webhook'а обновляет поля; повторный PUT поверх paused возобновляет доставку (statusactive).

Scope: chats.read

Request body:

{
  "url": "https://my-bot.example.com/robot-webhook",
  "secret_token": "Z3kVxyT9pBnQ4mLrE7sJfH2aD8gWcN5UvP6oR1XbCYq"
}
Поле Тип Обязательное Описание
url string да HTTPS URL вашего обработчика. Не более 2048 символов. См. «Требования к URL» ниже.
secret_token string да Секрет для аутентификации входящих POST'ов на вашей стороне. 32–256 символов из набора [A-Za-z0-9_-]. Рекомендуется secrets.token_urlsafe(32). В ответах API никогда не возвращается.

Требования к URL:

  • только https://;
  • порт — только 443 (или порт по умолчанию для HTTPS, без явного указания); нестандартные порты отвергаются;
  • хост — DNS-имя (IP-литералы — IPv4 и IPv6 — отвергаются);
  • DNS-имя не должно указывать на закрытые сетевые диапазоны;
  • без userinfo (https://user:pass@host/...) и без fragment (#...).

Response 200:

{
  "url": "https://my-bot.example.com/robot-webhook",
  "status": "active",
  "created_at": "2026-04-29T12:00:00Z",
  "updated_at": "2026-04-29T12:00:00Z",
  "last_delivery_at": null,
  "last_error_at": null,
  "last_error_status": null,
  "last_error_message": null,
  "paused_at": null
}

После первого PUT webhook получает только новые обновления. Если webhook был в статусе paused, повторный PUT снова включает доставку. Если за время паузы остались доступные обновления, они тоже придут.

Response 400 invalid_webhook_url — URL не прошёл валидацию.

Response 400 invalid_secret_tokensecret_token вне формата (длина < 32 / > 256 символов / символы вне набора).

GET /v1/chats/webhook — текущая конфигурация

Scope: chats.read

Response 200:

{
  "url": "https://my-bot.example.com/robot-webhook",
  "status": "active",
  "created_at": "2026-04-29T12:00:00Z",
  "updated_at": "2026-04-29T12:00:00Z",
  "last_delivery_at": "2026-04-29T13:42:01Z",
  "last_error_at": null,
  "last_error_status": null,
  "last_error_message": null,
  "paused_at": null
}
Поле Тип Описание
url string URL обработчика.
status string active — доставка идёт. paused — доставка временно остановлена после повторных неуспешных попыток.
created_at datetime Когда webhook был создан (UTC).
updated_at datetime Последний PUT (UTC).
last_delivery_at datetime | null Время последней успешной доставки (2xx).
last_error_at datetime | null Время последней неудачной попытки.
last_error_status int | null HTTP-статус последней неудачи. null, если timeout / network / TLS-ошибка.
last_error_message string | null Краткое описание последней неудачи (≤200 символов).
paused_at datetime | null Когда webhook поставлен в paused. null, если active.

secret_token в ответ не возвращается. Если вы потеряли его — выполните PUT /v1/chats/webhook заново с новым значением.

Response 404 webhook_not_set — webhook для робота не настроен.

DELETE /v1/chats/webhook — снять webhook

Scope: chats.read

Response 204 — запрос выполнен. Идемпотентно: если webhook'а не было, всё равно 204.

После DELETE:

  • доставка прекращается;
  • POST /v1/chats/updates снова доступен;
  • подписки на чаты сохраняются.

Повторный PUT /v1/chats/webhook создаст webhook заново и начнёт доставку с новых обновлений (как при первой установке).

Формат push-запроса

Robot выполняет POST {url} со следующими параметрами:

POST /robot-webhook HTTP/1.1
Host: my-bot.example.com
Content-Type: application/json; charset=utf-8
User-Agent: LocalHub-Robot-Webhook/1.0
X-Robot-Webhook-Secret-Token: Z3kVxyT9pBnQ4mLrE7sJfH2aD8gWcN5UvP6oR1XbCYq
X-Robot-Webhook-Attempt: 1
X-Robot-Webhook-Update-Id: 42

{
  "update_id": 42,
  "chat_id": "019560a1-aaaa-7def-8901-234567890abc",
  "type": "message",
  "received_at": "2026-04-17T12:05:01Z",
  "message": {
    "id": "019560a1-eeee-7def-8901-234567890abc",
    "sender": {
      "id": "019560a1-ffff-7def-8901-234567890abc",
      "login": "alex",
      "name": "Алексей"
    },
    "text": "Привет!",
    "sent_at": "2026-04-17T12:05:00Z"
  }
}

Тело — один объект Update, без обёртки массивом. Структура объекта совпадает с элементом updates[] в ответе POST /v1/chats/updates (см. «Структура обновления» выше).

Заголовок Значение
Content-Type application/json; charset=utf-8
User-Agent LocalHub-Robot-Webhook/1.0 (фиксированный, неконфигурируемый)
X-Robot-Webhook-Secret-Token secret_token, переданный в PUT /v1/chats/webhook. Передаётся только по HTTPS.
X-Robot-Webhook-Attempt Номер попытки доставки этого update_id (1, 2, 3, …).
X-Robot-Webhook-Update-Id Дублирует update_id из тела.

Что должен делать ваш обработчик

  • Сравнить X-Robot-Webhook-Secret-Token со своим хранимым значением с использованием постоянно-временного сравнения (например, hmac.compare_digest в Python).
  • Ответить любым 2xx-статусом. Тело ответа Robot не парсит и игнорирует. Размер ответа должен быть ≤ 64 KiB.
  • Уложиться в 10 секунд на ответ. Превышение → попытка считается неудачной, начинается retry.
  • Дедуплицировать входящие POST'ы по update_id: при повторных попытках доставки один update_id может прийти больше одного раза.

Robot не следует редиректам (3xx считается неудачей) и не поддерживает произвольные заголовки (Authorization и т.п.).

Повторные попытки доставки

При любой неудаче (статус не из 2xx, timeout, network/TLS-ошибка, ответ больше допустимого размера) Robot повторяет доставку того же update_id. Обработчик должен быть идемпотентным и корректно обрабатывать дубли.

После серии неуспешных попыток webhook переводится в status = "paused", а доставка останавливается до повторного PUT /v1/chats/webhook. Чтобы возобновить доставку, выполните PUT с тем же или новым secret_token / url. Накопленные за время паузы обновления могут быть доставлены, если они ещё доступны.

Если webhook долго не принимает успешные доставки, подписки робота могут быть сняты как неактивные. После возобновления webhook проверьте GET /v1/chats/subscriptions и при необходимости подпишите робота на чаты заново.

Типичные ошибки

401 — нет авторизации или токен невалиден

Заголовок Authorization отсутствует, не начинается с Bearer или токен невалиден, отозван либо истёк. Подробности приведены в разделе Быстрый старт.

403 permission_denied

У робота нет scope chats.read. В теле ответа поле details.required_scope содержит недостающее разрешение.

404 chat_not_found

Робот пытается подписаться или работать с чатом, который недоступен аккаунту, не поддерживается текущей версией API, либо для которого владелец-аккаунт не является создателем чата и не имеет права добавлять администраторов.

409 chat_already_subscribed

Другой робот уже подписан на этот чат. Снимите подписку у того робота и повторите запрос.

409 webhook_set

Для робота настроен webhook (любой статус — active или paused). Long polling и webhook-доставка взаимоисключающие. Чтобы вернуться к long polling, выполните DELETE /v1/chats/webhook.

404 webhook_not_set

GET /v1/chats/webhook или DELETE /v1/chats/webhook для робота, у которого webhook не настроен. (DELETE идемпотентен и возвращает 204 независимо от наличия записи; 404 возможен только для GET.)

400 invalid_webhook_url

URL не прошёл валидацию: не https://, нестандартный порт, IP-адрес вместо DNS-имени, DNS-имя резолвится в приватный диапазон, содержит userinfo или fragment, длиннее 2048 символов.

400 invalid_secret_token

secret_token имеет длину меньше 32 или больше 256 символов, либо содержит символы вне набора [A-Za-z0-9_-].

429 rate_limit_exceeded / monthly_quota_exceeded

Превышен лимит запросов. Увеличьте интервал между опросами, чтобы уменьшить количество запросов в периоды без новых событий.