Reference: Bot Webhook API
Справочник по доставке событий боту через push webhook (POST на URL бота).
Для концептуального обзора Bot account см. concepts/bots.md. Для отправки сообщений — reference/bot-rest-api.md. Для poll-альтернативы — reference/bot-event-poll.md.
Обзор
Bot Webhook — push-модель доставки событий. PartyFlow сам делает POST на HTTPS endpoint, который бот указывает в настройках. Это near-realtime: событие доставляется в течение секунд после его возникновения.
[PartyFlow] ──POST webhook──▶ [Bot HTTPS endpoint]
▲
└──────── 200 OK ────────┘Преимущества:
- Мгновенная доставка — не нужно ждать опроса.
- Меньше нагрузки на PartyFlow — нет постоянных GET-запросов.
Недостатки:
- Требует публичного HTTPS endpoint'а.
- Требует верификации HMAC-подписи (безопасность).
- Retry и backoff управляются PartyFlow — бот не контролирует порядок попыток.
Настройка бота
Чтобы включить webhook-доставку, при создании или обновлении бота установите:
| Поле | Тип | Описание |
|---|---|---|
event_delivery_mode |
string | "webhook" (по умолчанию "none"). |
webhook_url |
string | HTTPS URL, на который PartyFlow будет слать POST. Должен быть доступен из интернета. |
event_types[] |
string[] | События, на которые подписан бот. Например: ["MESSAGE_CREATED", "MESSAGE_UPDATED"]. |
channel_ids[] |
string[] | UUID каналов. Пусто = все каналы space'а. |
Установить можно через Admin UI (Integrations → Bots → Edit).
Ротация секрета
Для верификации подписи используйте webhook_signing_secret. Получить/обновить секрет можно через:
POST /api/v1/bot/webhook/rotate_secret
Authorization: Bearer fri_bot_<token>Ответ:
{
"signing_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"updated_at": "2026-05-01T12:00:00Z"
}Важно: секрет показывается только один раз при создании и при ротации. Сохраните его сразу.
Доставка событий
Запрос от PartyFlow
POST {webhook_url}
Content-Type: application/json
X-PartyFlow-Timestamp: 1714567890
X-PartyFlow-Signature: v1=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
User-Agent: PartyFlow-Webhook/1.0Тело:
{
"event_id": "01923f5c-a2c8-7890-b4d0-5a2c8a4b6e0c",
"event_type": "MESSAGE_CREATED",
"space_id": "sp-1",
"conversation_id": "conv-1",
"thread_id": "",
"actor_user_id": "u-1",
"schema_version": 1,
"occurred_at": "2026-05-01T12:00:00Z",
"data": {
"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
}
}
}| Поле | Тип | Описание |
|---|---|---|
event_id |
string (uuid) | Уникальный ID события. Используйте для idempotency. |
event_type |
string | Canonical тип: MESSAGE_CREATED, MESSAGE_UPDATED, REACTION_ADDED, ... |
space_id |
string | UUID space'а, в котором произошло событие. |
conversation_id |
string | UUID канала/конверсации. |
thread_id |
string | Legacy flat routing field из parent_message_id, если сообщение является reply. Для сообщений, отправленных напрямую в thread conversation, может быть пустой строкой; используйте data.conversation_context. |
actor_user_id |
string | UUID пользователя, инициировавшего событие. |
schema_version |
int | Версия схемы payload'а. |
occurred_at |
string (RFC3339) | Время возникновения события. |
data |
object | Event-specific payload. Для MESSAGE_CREATED это message_id, conversation_id, author_id, text, msg_index, sent_at, conversation_context, опционально thread_id, parent_message_id, mentions. Автор передаётся только плоским author_id; author.user_id не возвращается. |
conversation_context одинаковый для Bot Webhook, Bot Event Polling, Bot DM SSE и outgoing webhook data. scope принимает dm, public или private; для тредов он наследуется от родительской беседы. Call Thread из гостевого voice-чата приходит как type="thread" и subtype="call_thread".
Подпись и верификация
PartyFlow подписывает каждый запрос HMAC-SHA256. Вычисляйте подпись от:
timestamp + "." + raw_json_bodyГде timestamp — значение заголовка X-PartyFlow-Timestamp (unix seconds).
import hmac, hashlib, time
def verify_signature(secret: str, timestamp: str, body: bytes, signature_header: str) -> bool:
# 1. Проверить timestamp (±5 мин)
now = int(time.time())
ts = int(timestamp)
if abs(now - ts) > 300:
return False
# 2. Вычислить HMAC
signed_payload = f"{timestamp}.".encode() + body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
# 3. Сравнить константным временем
given = signature_header.removeprefix("v1=")
return hmac.compare_digest(expected, given)См. также concepts/security.md и guides/verify-signatures.md.
Ответ бота
| Статус | Смысл |
|---|---|
200–299 |
Успешно принято. PartyFlow считает доставку завершённой. |
3xx |
Считается ошибкой — PartyFlow не следует редиректам. |
4xx (кроме 429) |
Клиентская ошибка — не ретраится. Доставка помечается dead_letter. |
429 |
Rate limit — ретраится с exponential backoff. |
5xx |
Серверная ошибка — ретраится с exponential backoff. |
Тело ответа игнорируется (до 64 KiB читается для логов).
Retry и Auto-disable
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
delivery_timeout_seconds |
5 с | Таймаут HTTP-запроса. |
max_attempts |
8 | Максимальное число попыток. |
backoff_initial_seconds |
5 с | Первая задержка перед повтором. |
backoff_max_seconds |
3600 с (1 ч) | Потолок задержки. |
auto_disable_threshold |
100 | Последовательных неудач перед auto-disable режима webhook. |
Формула backoff: initial * 2^(attempt-1) с jitter ±20%.
При достижении auto_disable_threshold PartyFlow автоматически переводит event_delivery_mode бота в "none". Администратор может повторно включить webhook через Admin UI после исправления endpoint'а.
Сравнение способов доставки
| Критерий | Webhook | Poll API | Outgoing Webhook |
|---|---|---|---|
| Публичный endpoint | Нужен | Не нужен | Нужен |
| Задержка | Near-realtime | Частота опроса | Near-realtime |
| Retry | PartyFlow управляет | Бот управляет | PartyFlow управляет |
| HMAC-подпись | Обязательна | Не нужна | Обязательна |
| Масштабируемость | Push-модель | Линейная от числа ботов | Push-модель |
| Target | Bot account | Bot account | Space webhook |
SDK / Примеры
Python (FastAPI)
import hmac, hashlib, time
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@app.post("/bot/webhook")
async def bot_webhook(request: Request):
body = await request.body()
timestamp = request.headers.get("X-PartyFlow-Timestamp")
signature = request.headers.get("X-PartyFlow-Signature", "")
if not verify_signature(SECRET, timestamp, body, signature):
raise HTTPException(401, "invalid signature")
event = await request.json()
print(f"[{event['event_type']}] {event['event_id']}")
return {"ok": True}
def verify_signature(secret, timestamp, body, signature_header):
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.".encode() + body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
given = signature_header.removeprefix("v1=")
return hmac.compare_digest(expected, given)Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"time"
)
func verifyBotWebhook(secret, timestamp string, body []byte, signature string) bool {
ts, _ := strconv.Atoi(timestamp)
if abs(int(time.Now().Unix())-ts) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func abs(a int) int { if a < 0 { return -a }; return a }Наблюдаемость
Статус доставки, последние ошибки и auto-disable видны в Admin UI бота. Для production-сценариев настройте алерты на своей стороне receiver'а: долю 5xx, таймауты, p95 latency и повторные доставки по event_id.