PartyFlow

Reference: Bot Event Polling API#

Справочник по доставке событий боту через long-polling (GET /api/v1/bot/events).

Для концептуального обзора Bot account см. concepts/bots.md. Для отправки сообщений — reference/bot-rest-api.md.


Обзор#

Bot Event Polling — альтернатива outgoing webhook'ам. Вместо того чтобы PartyFlow делал POST на ваш URL, бот сам периодически забирает накопившиеся события через GET.

[Bot] ──GET /api/v1/bot/events──▶ [PartyFlow] ──events[]──▶ [Bot]
               ▲                                      │
               └───────── next_cursor ────────────────┘

Преимущества:

  • Не требует публичного HTTPS endpoint'а (удобно для dev, CI, on-prem).
  • Не требует HMAC-подписи и верификации — авторизация через Bearer token.
  • Простейший retry: если запрос упал, повторите с тем же cursor.

Недостатки:

  • Задержка доставки = частота опроса (рекомендуется 1–5 сек).
  • Больше нагрузки на PartyFlow при высокой частоте опроса.

Настройка бота#

Чтобы включить poll-доставку, при создании или обновлении бота установите:

Поле Тип Описание
event_delivery_mode string "poll" (по умолчанию "none").
event_types[] string[] События, на которые подписан бот. Например: ["MESSAGE_CREATED", "MESSAGE_UPDATED"].
channel_ids[] string[] UUID каналов. Пусто = все каналы space'а.

Интерактивные события (button.click, select.submit, modal.submit) и custom slash события (slash.command) доставляются owning-боту через poll queue автоматически, если у бота event_delivery_mode="poll". Добавлять их в event_types[] не нужно.

Установить можно через Admin UI (Integrations → Bots → Edit).


GET /api/v1/bot/events#

Забрать накопившиеся события.

Authentication#

Authorization: Bearer fri_bot_<token>

Тот же Bearer token, что и для отправки сообщений. См. reference/bot-rest-api.md.

Query parameters#

Параметр Тип По умолчанию Описание
cursor string "" ID последнего полученного события. Пусто — первая страница.
limit int 50 Сколько событий вернуть. Минимум 1, максимум 500.

Response (200)#

{
  "events": [
    {
      "id": "1234567890",
      "event_id": "01923f5c-a2c8-7890-b4d0-5a2c8a4b6e0c",
      "event_type": "MESSAGE_CREATED",
      "payload_json": "{\"message_id\":\"msg-1\",\"conversation_id\":\"conv-1\",\"author_id\":\"u-1\",\"text\":\"Hello world\",\"msg_index\":1042,\"sent_at\":\"2026-05-01T12:00:00.123456789Z\",\"conversation_context\":{\"conversation_id\":\"conv-1\",\"type\":\"channel\",\"scope\":\"public\",\"is_direct\":false,\"is_public\":true}}",
      "created_at": "2026-05-01T12:00:00Z"
    }
  ],
  "next_cursor": "1234567890"
}
Поле Тип Описание
id string (int64) Монотонный ID записи в очереди. Используется как cursor.
event_id string (uuid) Оригинальный ID события. Уникален в рамках (bot_id, event_id).
event_type string Canonical тип: MESSAGE_CREATED, MESSAGE_UPDATED, REACTION_ADDED, ... или direct bot тип button.click / select.submit / modal.submit / slash.command.
payload_json string JSON-строка с event-specific payload события. Клиент должен сделать JSON.parse(payload_json). Poll API не оборачивает payload в data и не возвращает data.message.
created_at string (RFC3339) Время появления события в очереди.
next_cursor string Передайте в следующем запросе как cursor. Пустая строка "" — больше нет событий.

Корневой объект события содержит только поля из примера выше: id, event_id, event_type, payload_json, created_at. В Poll API нет top-level type, нет объекта data и нет вложенного data.message.

Если новых событий нет, ответ остаётся успешным и явно возвращает пустой cursor:

{
  "events": [],
  "next_cursor": ""
}

Cursor pagination#

PartyFlow гарантирует стабильную сортировку по id. Используйте это поле как непрозрачный cursor и не завязывайтесь на его внутренний формат.

import requests, time
 
cursor = ""
while True:
    resp = requests.get(
        f"{BASE}/api/v1/bot/events",
        headers={"Authorization": f"Bearer {TOKEN}"},
        params={"cursor": cursor, "limit": 100},
    )
    resp.raise_for_status()
    data = resp.json()
 
    for ev in data["events"]:
        handle_event(ev)
 
    cursor = data.get("next_cursor", "")
    if not cursor:
        time.sleep(2)  # нет событий — подождём

Важно: события удаляются из очереди через retention_hours (по умолчанию 48 часов) после создания. Если бот не опрашивал дольше retention — события будут утрачены. Мониторьте next_cursor и не допускайте больших перерывов.

Ошибки#

Status Причина
400 Невалидный limit (вне диапазона 1..500) или бот не настроен на poll доставку.
401 Невалидный или отозванный Bearer token.
503 Функция Bot Event Polling отключена на инстансе.
500 Внутренняя ошибка. Ретраить с exponential backoff.

Payload событий#

payload_json — это строка с JSON-объектом. В Poll API top-level envelope уже вынесен в поля event_id, event_type, created_at. Для обычных message-событий внутри payload_json нет envelope-обёртки, а event-specific поля лежат напрямую: например, у MESSAGE_CREATED нет обёртки message, а текст сообщения лежит в поле text. Interactive-события используют тот же публичный payload, что callback mode, поэтому внутри payload_json есть event_type, space_id, actor_user_id и occurred_at.

const payload = JSON.parse(event.payload_json)

Пример MESSAGE_CREATED:

{
  "message_id": "msg-1",
  "conversation_id": "conv-1",
  "author_id": "u-1",
  "text": "Hello world",
  "msg_index": 1042,
  "sent_at": "2026-05-01T12:00:00.123456789Z",
  "mentions": ["bot-1"],
  "thread_id": "thread-root-message-id",
  "parent_message_id": "thread-root-message-id",
  "conversation_context": {
    "conversation_id": "conv-1",
    "type": "thread",
    "scope": "public",
    "subtype": "call_thread",
    "is_direct": false,
    "is_public": false,
    "parent_conversation_id": "voice-1",
    "parent_conversation_type": "voice",
    "parent_is_public": true
  }
}

conversation_id и parent_message_id всегда пишутся в snake_case. mentions присутствует только если в сообщении есть упоминания. thread_id и parent_message_id — legacy flat-поля для reply-сообщений, у которых реально есть parent_message_id; сообщение, отправленное напрямую в thread conversation, может не иметь этих flat-полей. Для определения треда используйте conversation_context.type="thread" и, при необходимости, conversation_context.parent_conversation_id.

conversation_context — канонический способ понять, куда пришло сообщение: type содержит raw conversation type (chat, channel, voice, thread или будущее значение как есть), scope — клиентский scope (dm, public, private). Для тредов scope наследуется от родителя, а subtype появляется только для специальных случаев: сейчас call_thread означает тред гостевого voice-чата. Flat-поля conversation_id, thread_id, parent_message_id сохраняются для обратной совместимости.

Автор сообщения всегда передаётся плоским полем author_id. Объект author или вложенное поле author.user_id в Poll API не возвращаются.

Interactive payload#

Если бот отправил сообщение с кнопками/списками и работает в poll mode, клик пользователя приходит в эту же очередь:

{
  "event_type": "button.click",
  "payload_json": "{\"interaction_id\":\"int-1\",\"event_type\":\"button.click\",\"space_id\":\"sp-1\",\"actor_user_id\":\"u-1\",\"conversation_id\":\"conv-1\",\"message_id\":\"msg-1\",\"action_id\":\"approve\",\"data\":\"{\\\"env\\\":\\\"prod\\\"}\",\"occurred_at\":\"2026-04-18T10:00:00Z\"}"
}

После JSON.parse(payload_json) бот получает interaction_id и canonical поля event_type, space_id, actor_user_id, occurred_at. Ответ нужно отправить отдельным запросом:

POST /api/v1/bot/interactions/{interaction_id}/respond

Body поддерживает update_message, send_message, ephemeral_text, open_modal или noop. modal.submit имеет те же поля контекста плюс values с отправленными пользователем значениями и не содержит data.

Interactive payload_json полностью повторяет callback body из reference/interactive-components.md:

Event Обязательные поля внутри payload_json
button.click interaction_id, event_type, space_id, actor_user_id, conversation_id, message_id, action_id, data, occurred_at; optional thread_id
select.submit interaction_id, event_type, space_id, actor_user_id, conversation_id, message_id, action_id, data, selected_value, occurred_at; optional thread_id
modal.submit interaction_id, event_type, space_id, actor_user_id, conversation_id, message_id, action_id, values, occurred_at; optional thread_id

Slash command payload#

Если poll-бот зарегистрировал custom slash command, invocation приходит как slash.command вне подписки event_types[]:

{
  "event_type": "slash.command",
  "payload_json": "{\"trigger_id\":\"trig-1\",\"command\":\"deploy\",\"command_id\":\"cmd-1\",\"space_id\":\"sp-1\",\"conversation_id\":\"conv-1\",\"user_id\":\"u-1\",\"text\":\"prod\",\"raw_text\":\"/deploy prod\",\"invoked_at\":\"2026-05-13T10:00:00Z\"}"
}

После JSON.parse(payload_json) бот отвечает финальным сообщением через:

POST /api/v1/bot/messages
X-PartyFlow-Trigger-Id: <trigger_id>

Body поддерживает content и optional metadata_json. Routing берётся из сохранённый trigger context: space, канал, тред и bot_id нельзя переопределить полями body.


Rate limiting#

Рекомендуемая частота опроса — не чаще 1 раза в секунду. PartyFlow может применять rate limit на GET /api/v1/bot/events (конфигурируется администратором инстанса). При превышении лимита — 429 Too Many Requests.


Сравнение с Outgoing Webhooks#

Критерий Poll API Outgoing Webhook
Публичный endpoint Не нужен Нужен
Задержка Частота опроса Near-realtime
Retry Простой (повторить запрос) PartyFlow управляет retry + backoff
Idempotency ON CONFLICT DO NOTHING X-PartyFlow-Delivery-Id
Масштабируемость Линейная от числа ботов Push-модель, эффективнее при высокой нагрузке

SDK / Примеры#

Python (requests)#

import os, requests, time
 
BASE = os.environ["PARTYFLOW_BASE"]
TOKEN = os.environ["BOT_TOKEN"]
 
cursor = ""
while True:
    r = requests.get(
        f"{BASE}/api/v1/bot/events",
        headers={"Authorization": f"Bearer {TOKEN}"},
        params={"cursor": cursor, "limit": 50},
        timeout=30,
    )
    r.raise_for_status()
    data = r.json()
    for ev in data["events"]:
        print(f"[{ev['event_type']}] {ev['payload_json']}")
    cursor = data.get("next_cursor", "")
    if not cursor:
        time.sleep(2)

Go#

Используйте стандартный net/http с тем же алгоритмом.