PartyFlow

Reference: Outgoing Webhooks#

Полный справочник для получателя событий из PartyFlow: payload, headers, retry, верификация подписи.

Для концептуального обзора (жизненный цикл, trigger modes, auto-disable) см. concepts/webhooks.md. Для best practices безопасности — concepts/security.md.


Обзор#

Outgoing webhook — подписка space'а на события PartyFlow. Когда в канале происходит событие (сообщение, реакция, создание канала), PartyFlow шлёт POST с JSON на URL, который вы указали при создании подписки. Получатель отвечает 2xx, чтобы подтвердить доставку.

[PartyFlow event] ──POST──▶ [your HTTPS endpoint] ──2xx──▶ done

Создание подписки#

Подписка создаётся admin'ом space'а в UI PartyFlow. При создании вы указываете:

Поле Описание
name Человекочитаемое имя. Для admin UI.
url HTTPS URL, на который будут приходить POST. SSRF-фильтр отвергает приватные диапазоны (RFC 1918, loopback, link-local).
event_types[] Хотя бы один event type из списка ниже.
channel_ids[] UUID'ы каналов. Пусто = все каналы space'а.
trigger all / mention / keywords. Для MESSAGE_CREATED/MESSAGE_UPDATED.
trigger_config JSON, зависит от trigger (см. concepts/webhooks.md).
include_context + context_size Подтягивать ли последние N сообщений канала в payload (1..50). Для AI/LLM.

После создания вы получаете signing secret — показывается один раз. Сохраните в secrets manager немедленно.


Request от PartyFlow#

Каждая доставка — отдельный HTTP-запрос:

POST https://your-service.example.com/webhooks/partyflow
Content-Type: application/json
User-Agent: PartyFlow-Webhook/1.0
X-PartyFlow-Event: MESSAGE_CREATED
X-PartyFlow-Delivery-Id: 01923f5c-a2c8-7890-b4d0-5a2c8a4b6e0c
X-PartyFlow-Timestamp: 1744934400
X-PartyFlow-Signature: sha256=2c1a9f3e8b7d4a6c5e2f1d0b3a9c8e7f4d5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c
X-PartyFlow-Webhook-Version: 1
 
<JSON body, см. ниже>

Headers#

Header Назначение
Content-Type Всегда application/json
User-Agent PartyFlow-Webhook/1.0
X-PartyFlow-Event Canonical event type (MESSAGE_CREATED, REACTION_ADDED, ...). Быстрая диспетчеризация без парсинга body.
X-PartyFlow-Delivery-Id UUIDv7, уникален для каждой доставки. Стабилен на все retry одной и той же доставки. Используйте для идемпотентности.
X-PartyFlow-Timestamp Unix seconds. Используется в signing string. Проверяйте разрыв с now() ≤ 5 минут, чтобы отвергать replay.
X-PartyFlow-Signature sha256=<hex(HMAC-SHA256)> от v1:{timestamp}:{body} с вашим signing secret.
X-PartyFlow-Webhook-Version Мажорная версия схемы payload'а. Сейчас 1. PartyFlow передаёт её как строку — при major-bump'е вы можете ветвить парсинг без чтения тела.

Timestamp, Delivery-Id и Signature фиксируются перед первой отправкой и не меняются между retry. Это означает, что HMAC одной и той же доставки всегда сходится — даже если admin ротирует signing secret между попытками.

Body: envelope#

{
  "event_id": "01923f5c-9876-7000-b4d0-5a2c8a4b6e0c",
  "event_type": "MESSAGE_CREATED",
  "space_id": "550e8400-e29b-41d4-a716-446655440000",
  "conversation_id": "660e8400-e29b-41d4-a716-446655440001",
  "thread_id": "",
  "actor_user_id": "770e8400-e29b-41d4-a716-446655440002",
  "occurred_at": "2026-04-18T14:32:05.123Z",
  "schema_version": 1,
  "data": { /* event-specific, см. ниже */ },
  "context": { /* опционально, только если include_context=true */ }
}

Поля envelope:

Поле Тип Описание
event_id UUIDv7 Уникальный ID исходного события. Одно событие → одна доставка на подписку. Одно и то же событие может получить несколько подписокevent_id у них одинаковый, delivery_id — разный.
event_type string Canonical event name (см. таблицу ниже).
space_id UUID Space, в котором случилось событие.
conversation_id UUID Беседа, где произошло событие. Для thread-сообщения это ID треда; родитель доступен в data.conversation_context.parent_conversation_id. Пусто для space-scope событий.
thread_id UUID | "" Thread root message ID, если событие внутри треда. Пусто иначе.
actor_user_id UUID | "" Кто инициировал событие (автор сообщения, поставивший реакцию). Пусто для системных событий.
occurred_at ISO 8601 UTC Время события. Timezone — всегда UTC (Z).
schema_version int Версия схемы envelope'а. Начинается с 1, будет повышаться при breaking changes.
data object Event-specific payload. Может быть null для событий без данных.
context object? Присутствует только если у подписки include_context=true. См. Context enrichment.

Body: per-event data#

Поле data — сериализованный event-specific payload. Текущий формат:

MESSAGE_CREATED, MESSAGE_UPDATED#

{
  "message_id": "...",
  "conversation_id": "...",
  "thread_id": "...",
  "parent_message_id": "...",
  "text": "Deploy to prod starting now",
  "mentions": ["bot_id_1", "user_id_2"],
  "msg_index": 1042,
  "author_id": "...",
  "sent_at": "2026-04-18T14:32:05.123456789Z",
  "conversation_context": {
    "conversation_id": "...",
    "type": "thread",
    "scope": "public",
    "subtype": "call_thread",
    "is_direct": false,
    "is_public": false,
    "parent_conversation_id": "...",
    "parent_conversation_type": "voice",
    "parent_is_public": true
  }
}
  • conversation_id — канал/конверсация, где находится сообщение.
  • thread_id и parent_message_id — legacy flat-поля для reply-сообщений, у которых реально есть parent_message_id. Сообщение, отправленное напрямую в thread conversation, может не иметь этих flat-полей; определяйте тред по conversation_context.type="thread" и parent-полям внутри conversation_context.
  • mentions[] — ID'ы упомянутых участников (через @name).
  • msg_index — монотонный counter в канале. Используется для unread-логики в UI.
  • author_id — ID автора сообщения. Вложенный объект author / author.user_id не используется.
  • sent_at — время отправки сообщения в UTC.
  • conversation_context — канонический контекст беседы для ботов и webhook-получателей. type — raw conversation type (chat, channel, voice, thread или будущее значение), scopedm, public или private. Для тредов scope берётся от родителя; subtype="call_thread" ставится только для Call Thread гостевого voice-чата.
  • Для MESSAGE_UPDATED также может присутствовать edited_at.

MESSAGE_DELETED#

{
  "message_id": "...",
  "conversation_id": "...",
  "text": "",
  "msg_index": 1042,
  "author_id": "...",
  "sent_at": "2026-04-18T14:32:05.123456789Z",
  "conversation_context": {
    "conversation_id": "...",
    "type": "channel",
    "scope": "private",
    "is_direct": false,
    "is_public": false
  }
}

Тело удалённого сообщения не передаётся: text приходит пустой строкой. Это сохраняет routing-поля (message_id, conversation_id, msg_index) без раскрытия удалённого содержимого.

REACTION_ADDED, REACTION_REMOVED#

{
  "message_id": "...",
  "conversation_id": "...",
  "emoji": "+1",
  "user_id": "..."
}

CONVERSATION_CREATED, CONVERSATION_ARCHIVED#

{
  "conversation_id": "...",
  "name": "deploy-notifications",
  "type": "public",
  "created_by": "..."
}

Примечание: точные field names data могут отличаться по историческим причинам — парсите защищённо (игнорируйте неизвестные поля, используйте optional-доступ). Envelope-уровень (event_id, event_type, space_id, ...) стабилен.

Context enrichment#

Если подписка создана с include_context=true, payload содержит дополнительный блок:

{
  "event_id": "...",
  "event_type": "MESSAGE_CREATED",
  "data": { ... },
  "context": {
    "messages": [
      {
        "id": "...",
        "content": "Previous message in the thread",
        "author_id": "...",
        "created_at": "2026-04-18T14:30:10.000Z",
        "edited_at": "2026-04-18T14:30:15.000Z"
      }
    ],
    "context_error": false
  }
}
  • messages[] — до context_size последних сообщений канала до триггерного события. Полезно для AI/LLM, которым нужен контекст для ответа.
  • context_error: true — попытка enrichment'а не удалась (transient ошибка между integration и chat). Сообщение всё равно доставлено, но без контекста; решайте на своей стороне, нужно ли prompting пользователя про retry.
  • edited_at присутствует только если сообщение было отредактировано.

Response от получателя#

Отвечайте как можно быстрее — разумный target < 1 сек, жёсткий timeout client'а — несколько секунд.

Коды#

Статус Что произойдёт
2xx Success. Доставка помечена как успешная, consecutive_failures сбрасывается.
408, 429, 5xx Retry по exponential schedule (см. Retry schedule ниже). После исчерпания попыток → dead-letter.
429 или 503 с Retry-After Вместо exponential PartyFlow ждёт указанное время (clamp 1s..1h). RFC 7231 — delta-seconds или HTTP-date.
Остальные 4xx (400, 401, 403, 404, 422, ...) Dead-letter немедленно — нет смысла ретраить, payload/URL/auth misconfig'ят. consecutive_failures инкрементируется.
Transport error (DNS, TCP, TLS) Retry как 5xx.

Retry-After форматы#

PartyFlow читает Retry-After в следующем порядке:

  1. HTTP header Retry-After: 30 (delta-seconds).
  2. HTTP header Retry-After: Wed, 18 Apr 2026 14:35:00 GMT (HTTP-date).
  3. Body JSON {"retry_after": 30} — legacy fallback для сервисов, не умеющих заголовки.
  4. Body — просто число 30 в plaintext.

Диапазон — [1s, 1h]. Значения вне диапазона игнорируются → fallback на exponential.

Требования к receiver'у#

  1. Idempotency по X-PartyFlow-Delivery-Id. Если вы уже обработали это delivery_id, не делайте side effect повторно — просто отвечайте 2xx. Retry может прийти через час и более.
  2. Верификация подписи до любой бизнес-логики. См. guides/verify-signatures.md.
  3. Constant-time compare при сверке подписей (защита от timing attack).
  4. Возвращайте 2xx быстро. Если обработка тяжёлая — ставьте задачу в свою очередь и сразу отвечайте. Иначе словите 408-подобный timeout с нашей стороны и retry.
  5. Не ретраить 4xx. Если вы возвращаете 400 — PartyFlow не повторит запрос с этим payload'ом никогда.

Retry schedule#

Расписание — экспоненциальный backoff, конфигурируемый на стороне платформы. Формула:

delay_before_attempt(N+1) = min(Initial · 2^(N-1), Max) ± Jitter

Текущие production-дефолты: Initial = 5s, Max = 1h, MaxAttempts = 8 (первая попытка + 7 ретраев), Jitter = ±20%.

Попытка Задержка (база) Общее время с момента события
1 сразу T0
2 5s ±20% ~T0 + 5s
3 10s ±20% ~T0 + 15s
4 20s ±20% ~T0 + 35s
5 40s ±20% ~T0 + 1m 15s
6 80s ±20% ~T0 + 2m 35s
7 160s ±20% ~T0 + 5m 15s
8 320s ±20% ~T0 + 10m 35s
dead-letter

Полное retry-window от первой попытки до dead-letter — ~10 минут 35 секунд базы; с учётом симметричного ±20% jitter'а — примерно [8m 28s, 12m 42s]. Clamp Max = 1h при текущих значениях не срабатывает (максимальная задержка в серии — 320s), но активируется, если оператор увеличит Initial или MaxAttempts.

Jitter симметричный — задержка в [0.8 × base, 1.2 × base].

Значения Initial, Max, MaxAttempts могут быть скорректированы оператором платформы. Ориентируйтесь на полное retry-window, а не на конкретное число попыток — привязка к "после 4-й неудачной — точно dead-letter" не гарантирована.

Если circuit breaker сработал (подписка много раз подряд упала), retry автоматически откладывается на ~30 секунд без траты attempts. Rate limit (per-plan) тоже откладывает без траты attempts.


Auto-disable#

Когда подписка получает 100 подряд dead-letter'ов, она автоматически деактивируется:

  • Подписка получает статус inactive.
  • Admin space'а получает уведомление в UI.
  • Новые события больше не попадают в очередь этой подписки до ручной активации.

Счётчик consecutive failures сбрасывается при первом 2xx. События, случившиеся пока подписка была disabled, не буферизуются — они не будут доставлены после включения.


Верификация подписи (краткая версия)#

Полные snippets — в guides/verify-signatures.md. Кратко:

import hmac, hashlib, time
 
def verify(secret: str, ts: str, body: bytes, sig_header: str) -> bool:
    if not ts.isdigit():
        return False
    if abs(int(time.time()) - int(ts)) > 300:
        return False
    if not sig_header.startswith("sha256="):
        return False
    base = f"v1:{ts}:".encode() + body
    expected = hmac.new(secret.encode(), base, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig_header[len("sha256="):])

Ключевые моменты:

  • Signing string — v1:{timestamp}:{raw_body_bytes}. Префикс v1: — версия схемы.
  • Используйте raw_body_bytes как они пришли, без re-parse / re-serialize JSON.
  • Сначала проверяйте timestamp window, потом HMAC — экономит CPU на replay attempts.
  • hmac.compare_digest (constant-time) обязателен.

Event types enum#

Canonical name Доставляется
MESSAGE_CREATED
MESSAGE_UPDATED
MESSAGE_DELETED
REACTION_ADDED
REACTION_REMOVED
CONVERSATION_CREATED
CONVERSATION_ARCHIVED
MEMBER_JOINED Planned — не публикуется в текущем релизе.
MEMBER_LEFT Planned — не публикуется в текущем релизе.

Подписаться на MEMBER_JOINED/MEMBER_LEFT можно уже сейчас, но события не будут приходить, пока publisher не включит их в следующем релизе.


Ограничения#

  • URL — только HTTPS в production. HTTP допустим только в dev.
  • SSRF блокировка: приватные диапазоны (RFC 1918, loopback, link-local) отвергаются при настройке URL и перед каждой отправкой (через отдельный DNS-resolve).
  • Payload size — не декларирован публично; envelope остаётся компактным (обычно < 10 KB), но context enrichment может добавить до 50 сообщений с текстом.
  • Параллельные доставки от одной подписки возможны. Если порядок критичен — обрабатывайте на своей стороне с учётом event_id и occurred_at.
  • Context enrichmentcontext_size ограничен 1..50. Больше не поддерживается.
  • Keywords trigger — до 20 ключевых слов на подписку, каждое 2..100 символов.

Управление подписками в Admin UI#

Admin'ы space'а управляют outgoing webhook'ами в разделе Integrations → Outgoing webhooks. Для операций нужна роль admin или owner; обычные участники не видят управление подписками.

Доступные действия:

Операция Что делает
Создание Создаёт подписку и показывает plaintext signing_secret один раз.
Список и детали Показывает подписки space'а, фильтры, статус и последние доставки.
Обновление Меняет URL, события, каналы, trigger, context и активность подписки.
Удаление Отключает подписку и удаляет запланированные доставки.
Тестовая отправка Отправляет пробный POST на URL подписки.
Ротация секрета Выдаёт новый plaintext signing_secret один раз; URL не меняется.
Лог доставок Показывает историю отправок с фильтром all / success / failed.

После создания или ротации сохраните signing_secret сразу: повторно получить plaintext нельзя. В списках, деталях и уведомлениях секрет не отображается.


Что дальше#