PartyFlow

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.

Ответ бота#

Статус Смысл
200299 Успешно принято. 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.