Reference: Interactive Components
Аудитория этого документа — разработчики ботов.
Позволяет боту добавлять кнопки, select menus и модальные формы в сообщения. При клике PartyFlow доставляет interactive-событие либо синхронным HMAC POST на callback URL, либо через Bot Poll API, если у бота включён event_delivery_mode="poll".
Применение: approval flows, опросы, выбор окружения для деплоя, быстрые действия.
Base URL
https://api.partyflow.ruВо всех примерах — переменная $BASE.
Authentication
Все interactive-запросы бота используют Bearer token того же бота, что и в Bot REST API:
Authorization: Bearer fri_bot_<token>Для callback mode отдельно от signing_secret у бота есть buttons_signing_secret — подпись исходящих click-запросов. Генерируется при первой настройке callback URL и показывается один раз. В poll mode внешний секрет для кликов не нужен: бот получает события через авторизованный GET /api/v1/bot/events.
Setup flow
Есть два режима доставки:
- Callback mode: бот вызывает
PATCH /api/v1/bot/interactive_configсcallback_url, получаетsigning_secret, затем PartyFlow шлёт HMAC POST с eventbutton.click/select.submit/modal.submit. - Poll mode: бот включает
event_delivery_mode="poll", отправляет сообщения сmetadata_json, а клики забирает черезGET /api/v1/bot/events. В payload будетinteraction_id; ответ отправляется черезPOST /api/v1/bot/interactions/{interaction_id}/respond.
В обоих режимах бот может вернуть update_message / send_message с новым metadata_json, чтобы обновить кнопки или показать следующий шаг.
Config endpoints
PATCH /api/v1/bot/interactive_config
Request:
{"callback_url": "https://your-bot.example.com/partyflow/interactive"}Response при первой настройке (200):
{
"ok": true,
"callback_url": "https://your-bot.example.com/partyflow/interactive",
"signing_secret": "dead...beef"
}
signing_secretвозвращается один раз. Для ротации —POST /api/v1/bot/interactive_config/rotate_secret.
Response при повторной настройке URL (200):
{"ok": true, "callback_url": "https://..."}signing_secret НЕ возвращается — используется тот же.
GET /api/v1/bot/interactive_config
Статус interactive config (без секрета):
{"ok": true, "enabled": true, "callback_url": "https://..."}POST /api/v1/bot/interactive_config/rotate_secret
Генерирует новый signing_secret, возвращает plaintext один раз. Все активные кнопки инвалидируются (их signed_token больше не проходит verify).
DELETE /api/v1/bot/interactive_config
Отключает interactive для бота. Все активные кнопки становятся неработающими.
Откуда берутся сообщения с кнопками
Разместить интерактивные блоки в канале можно тремя путями:
- Через
POST /api/v1/bot/messagesсmetadata_json— бот сам отправляет сообщение с блоками. Подходит для approval flows, interactive-опросов, deploy-команд. Пример ниже. - Через incoming webhook — внешний сервис (CI, alerting, approval-система) шлёт сообщение в канал через
POST /api/v1/webhooks/incoming/{token}, payload включает блоки. PartyFlow подпишет action tokens и доставит сообщение. - В ответ на
button.click/modal.submit— в response-пакете бот возвращаетupdate_message/send_messageс новымmetadata_json, содержащим блоки. Типично для follow-up: после первого клика показать форму или обновлённое состояние. См. раздел Response (bot → PartyFlow).
Пример: отправить сообщение с кнопками через REST
Быстрый рабочий пример для approval flow: две кнопки «Approve» и «Reject» в канале.
BASE='https://api.partyflow.ru'
TOKEN='fri_bot_...'
CHANNEL_ID='<uuid>'
# metadata_json — строка с JSON-схемой блоков (сериализуется отдельно от
# внешнего payload). signed_token для каждого action_id PartyFlow
# проставит автоматически при записи сообщения.
METADATA=$(cat <<'JSON'
{
"version": 1,
"blocks": [
{
"type": "actions",
"elements": [
{
"type": "button",
"action_id": "approve",
"text": "Approve",
"style": "primary",
"data": "{\"release\":\"v2.3.0\"}"
},
{
"type": "button",
"action_id": "reject",
"text": "Reject",
"style": "danger",
"data": "{\"release\":\"v2.3.0\"}"
}
]
}
]
}
JSON
)
curl -X POST "$BASE/api/v1/bot/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg cid "$CHANNEL_ID" --arg meta "$METADATA" '{
conversation_id: $cid,
content: "Release v2.3.0 готов к выкатке в prod. Подтверди или отклони.",
metadata_json: $meta
}')"Ожидаемый ответ:
{"ok": true, "message_id": "<uuid>"}Важно:
metadata_json— строка с JSON внутри, не вложенный объект. Сериализуется отдельно от основного payload (см.jq -n --arg meta …выше).action_idдолжны быть уникальны в рамках сообщения.data— opaque для PartyFlow строка; туда можно положить ID релиза, окружение, что угодно — она придёт обратно в click payload без изменений.- Перед первой отправкой сообщения с кнопками нужен один из режимов доставки:
callback_urlчерезPATCH /api/v1/bot/interactive_configилиevent_delivery_mode="poll"у бота.
Пошаговое onboarding — см. guides/bot-interactive-quickstart.md.
Полный список лимитов полей, styles и опций — ниже в разделе Schema.
Schema (bot_metadata_json)
Schema, которой бот описывает интерактивные блоки (в incoming webhook payload или в response update_message / send_message):
{
"version": 1,
"blocks": [
{
"type": "actions",
"elements": [
{
"type": "button",
"action_id": "approve",
"text": "Approve",
"style": "primary",
"data": "{\"env\":\"prod\"}",
"visibility_user_ids": ["u-1", "u-2"],
"confirm": {
"title": "Подтвердите",
"text": "Deploy to prod?",
"confirm_label": "Deploy",
"cancel_label": "Cancel"
}
},
{
"type": "select",
"action_id": "pick_env",
"placeholder": "Environment",
"options": [
{"value": "prod", "label": "Production"},
{"value": "staging", "label": "Staging"}
]
}
]
}
]
}Лимиты
| Поле | Ограничение |
|---|---|
blocks |
до 5 |
elements per block |
до 5 (итого 25 on message) |
action_id |
^[a-z0-9_-]{1,64}$, уникальный в рамках сообщения |
text (label) |
до 75 символов |
data |
до 2000 символов |
placeholder |
до 100 символов |
Select options |
до 25 опций |
Option value |
до 150 символов, уникальный |
Option label |
до 75 символов |
confirm.title |
до 75 |
confirm.text |
до 300 |
visibility_user_ids |
до 100 user_ids |
Весь bot_metadata_json |
до 32 KiB |
Styles
primary— основная (синяя)danger— деструктивная (красная)default/ unset — обычная (нейтральная)
Signed token
PartyFlow автоматически подписывает каждый action_id на send/edit — добавляет поле signed_token в JSON. В текущем контракте token связан с (buttons_signing_secret, action_id, data, exp) и проверяется при клике; default TTL — 24 часа (конфигурируется оператором). message_id не входит в HMAC-binding, поэтому bots должны обрабатывать клики идемпотентно.
Не нужно генерировать signed_token на стороне бота — PartyFlow делает это автоматически.
Click payload (PartyFlow → bot)
В callback mode PartyFlow отправляет HTTP POST:
POST https://your-bot.example.com/partyflow/interactive
Content-Type: application/json
X-PartyFlow-Signature: sha256=<hex>
X-PartyFlow-Timestamp: 1713434400
X-PartyFlow-Delivery-Id: 8b4c7f2a-...
X-PartyFlow-Event: button.click // или select.submit / modal.submit
X-PartyFlow-Webhook-Version: 1Interactive payload использует canonical flat-поля event_type,
actor_user_id, occurred_at. Старые имена type, user_id, timestamp
не отправляются.
Body (button.click):
{
"event_type": "button.click",
"action_id": "approve",
"data": "{\"env\":\"prod\"}",
"actor_user_id": "...",
"space_id": "...",
"conversation_id": "...",
"message_id": "...",
"thread_id": "...",
"occurred_at": "2026-04-18T..."
}В poll mode этот же JSON лежит внутри payload_json события
event_type="button.click" / select.submit, плюс добавляется
interaction_id:
{
"interaction_id": "uuid",
"event_type": "button.click",
"action_id": "approve",
"data": "{\"env\":\"prod\"}",
"actor_user_id": "...",
"space_id": "...",
"conversation_id": "...",
"message_id": "...",
"occurred_at": "..."
}Body (select.submit):
{
"event_type": "select.submit",
"action_id": "pick_env",
"data": "",
"actor_user_id": "...",
"space_id": "...",
"conversation_id": "...",
"message_id": "...",
"selected_value": "prod",
"occurred_at": "..."
}Payload fields
| Поле | button.click | select.submit | modal.submit | Источник / описание |
|---|---|---|---|---|
event_type |
да | да | да | Тип события: button.click, select.submit, modal.submit. Дублирует X-PartyFlow-Event, чтобы body можно было обрабатывать без headers. |
space_id |
да | да | да | Server-authoritative space, которому принадлежит исходное сообщение. |
actor_user_id |
да | да | да | Пользователь, который кликнул кнопку/select или отправил modal. |
conversation_id |
да | да | да | Беседа исходного bot-сообщения. |
message_id |
да | да | да | Исходное bot-сообщение с interactive block. |
action_id |
да | да | да | ID action из metadata/schema. Для modal это action исходного клика, который открыл форму. |
data |
да | да | нет | Opaque строка из button/select element. Может быть пустой строкой. |
selected_value |
нет | да | нет | Выбранное значение select menu. |
values |
нет | нет | да | JSON object со значениями modal fields. Если форма пустая, приходит {}. |
thread_id |
опционально | опционально | опционально | Parent/root message ID, если исходное сообщение было в thread/reply context. |
occurred_at |
да | да | да | UTC timestamp в RFC3339/RFC3339Nano. |
interaction_id |
poll only | poll only | poll only | Есть только внутри payload_json в poll mode; нужен для POST /api/v1/bot/interactions/{interaction_id}/respond. |
Response (bot → PartyFlow)
В callback mode бот отвечает sync JSON в течение 3 секунд. В poll mode бот отправляет тот же JSON в:
POST /api/v1/bot/interactions/{interaction_id}/respondПоддерживаются 5 типов ответа (взаимоисключающие):
1. update_message — обновить текущее сообщение
{
"update_message": {
"text": "Approved by @user",
"metadata_json": "{\"version\":1,\"blocks\":[...]}"
}
}Используется для approval flow: после клика убрать кнопки и показать итог.
2. send_message — новое сообщение в канал
{
"send_message": {
"text": "Deploy started at 10:45",
"metadata_json": "..."
}
}3. ephemeral_text — только клиенту
{"ephemeral_text": "У вас нет прав для этого действия."}Видит только нажавший — не сохраняется в канале.
4. open_modal — открыть форму
{
"open_modal": {
"title": "Deploy config",
"fields": [
{"id": "reason", "type": "text", "label": "Reason", "required": true},
{"id": "notify", "type": "checkbox", "label": "Notify channel"}
],
"submit_label": "Deploy"
}
}Bot владеет schema (PartyFlow её не валидирует — просто передаёт клиенту).
5. noop — пустое тело или {"noop":true}
Bot уже сделал работу и ничего показывать не нужно.
Modal submit flow
- Bot получает
button.click→ возвращаетopen_modal. - PartyFlow генерирует single-use
modal_token(TTL 15 мин) и отдаёт клиенту. - Client показывает модалку, пользователь заполняет, submit.
- PartyFlow шлёт на callback URL event
modal.submit:
{
"event_type": "modal.submit",
"action_id": "deploy_config",
"actor_user_id": "...",
"space_id": "...",
"conversation_id": "...",
"message_id": "...",
"values": {"reason": "Fix critical bug", "notify": true},
"occurred_at": "..."
}- Bot отвечает
update_message/send_message/ephemeral_text/noop. В poll mode также поддерживаетсяopen_modalпослеmodal.submit, чтобы строить цепочки модалок; глубина ограничена настройкой инстанса.
HMAC verification
Схема: v1:{timestamp}:{raw_body} → HMAC-SHA256 → hex → X-PartyFlow-Signature: sha256=<hex>.
Идентично outgoing webhooks и slash commands. Ключ — buttons_signing_secret (не signing_secret от webhooks).
Python example:
import hmac, hashlib, time
def verify_interactive(secret, timestamp, body, signature):
if abs(int(time.time()) - int(timestamp)) > 300:
return False
base = f"v1:{timestamp}:{body.decode()}".encode()
expected = "sha256=" + hmac.new(secret.encode(), base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Rate limits
| Уровень | Лимит |
|---|---|
| per-user × message | 30 кликов/мин (anti double-click spam) |
| per-bot global | 600 кликов/мин (anti stampede на callback endpoint) |
Плюс idempotency guard: повторный клик одной и той же кнопки тем же пользователем в течение 60 сек возвращает 200 OK без повторного POST на бота.
Visibility / per-user access
visibility_user_ids ограничивает кто может кликать кнопку. Список из 1..100 user_ids. Пустой = все участники канала. Проверка выполняется на стороне PartyFlow: user не в списке → 403.
Использование: approval от назначенного approver'а, restricted actions по ролям.
Error paths
| Ошибка | HTTP | Сценарий |
|---|---|---|
| 400 | Invalid JSON / schema violation | |
| 401 | Invalid/missing session token | |
| 403 | Cross-bot edit / visibility denied | |
| 410 Gone | Message deleted / action_id больше не рендерится / modal_token expired | |
| 429 | Rate limit tripped | |
| 503 | Callback URL недоступен / rate-limiter circuit breaker |
Best practices
- Отвечай быстро. 3 секунды — жёсткий лимит.
- Idempotency. Bot должен сам быть idempotent — один и тот же
action_id + actor_user_idможет прилететь дважды при network retry. - Храни
buttons_signing_secretв secret manager. - Не читай
message_id/conversation_idиз body — они уже server-verified. - Не сохраняй
dataдля долгих сценариев (token TTL 24h). - Используй
visibility_user_idsдля approval flows с назначенным approver'ом. noopдля silent ack когда action уже выполнен другим путём.