Чаты¶
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 |
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_error—external_user_idпустая строка или длиннее 128 символов.403 permission_denied— у робота нет scopechats.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 (по умолчанию) — ответ возвращается немедленно. 1–30 — если доступных обновлений нет, сервер удерживает соединение до появления нового обновления для этого робота или до истечения 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, для message — null. Это сам 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 одновременно задаёт нижнюю границу выдачи и подтверждает ранее
обработанные обновления.
- Первый запрос выполните без
offsetили сoffset=0. - Обработайте полученные обновления и сохраните максимальный
update_id. - Следующий запрос выполните с
offset = max(update_id) + 1. - Повторяйте цикл с задержкой между запросами.
Подтверждённое обновление не возвращается повторно
После запроса с 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 для разовых проверок, где не нужен непрерывный цикл:
Для непрерывного получения сообщений используйте 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 возобновляет доставку (status → active).
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_token — secret_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
Превышен лимит запросов. Увеличьте интервал между опросами, чтобы уменьшить количество запросов в периоды без новых событий.