PartyFlow

Reference: Incoming Webhook HTTP Endpoint#

Полный справочник endpoint'а, который принимает сообщения от внешних сервисов.


Endpoint#

POST /api/v1/webhooks/incoming/{token}

{token} — уникальная часть URL, выдаётся при создании webhook'а. Хранить как secret (даже если require_signature: false — это единственный фактор защиты).


Request#

Headers#

Header Обязателен Значение
Content-Type Да application/json или application/x-www-form-urlencoded
X-Slack-Signature Если require_signature: true и выбрана Slack-схема v0=<hex(HMAC-SHA256)>
X-Slack-Request-Timestamp То же Unix seconds
X-PartyFlow-Signature Если require_signature: true и выбрана PartyFlow-native sha256=<hex(HMAC-SHA256)>
X-PartyFlow-Timestamp То же Unix seconds

Если Content-Type: application/x-www-form-urlencoded, endpoint извлекает поле payload и парсит его как JSON (Slack interactive pattern).

Body#

Максимум 256 KB. Endpoint определяет формат по структуре payload:

Признак Формат
Есть хотя бы одно из text, attachments, blocks Slack
Есть message.content или message.entity_type Pachca
Ни то ни другое HTTP 400

Slack-формат#

Поле Тип Описание
text string Текст (Slack mrkdwn → конвертируется в стандартный Markdown)
attachments array Legacy attachments (color bar, title, fields, footer)
blocks array Block Kit: header, section, divider, context, image
username string Переопределяет display name webhook'а для этого сообщения
icon_url string Переопределяет аватар для этого сообщения (https-only)
silent bool См. "Push policy"
severity string critical / warning / info — см. "Push policy"
mentions string[] user_id получателей push'а

Полное описание attachments и blocksreference/message-format.md.

Конвертация mrkdwn → Markdown (применяется только к text, не к тексту внутри attachments/blocks):

Slack mrkdwn Markdown
*bold* **bold**
_italic_ *italic*
~strike~ ~~strike~~
<url|text> [text](url)
<url> url
&amp; &lt; &gt; & < >

Pachca-формат#

Поле Тип Описание
message.content string Markdown (передаётся as-is, без mrkdwn-конвертации)
message.buttons array URL-кнопки
message.files array Вложения
silent, severity, mentions см. Slack-формат Работают идентично; допустимы и на верхнем уровне, и внутри message.*

Response#

Success#

HTTP 200 OK
Content-Type: application/json
 
{"ok": true, "message_id": "<uuid>"}

Сообщение появляется в канале в течение ~100–500ms после 200.

Errors#

Status Причина Ретраить?
400 Невалидный JSON; неизвестный формат (нет text/attachments/blocks/message.content); поле некорректного типа. Нет — фиксить payload.
401 Неверный/отозванный token; невалидная подпись; timestamp вне окна 5 минут. Нет — проверить token/secret/часы.
405 Метод != POST. Нет.
410 Webhook auto-disabled после 100 подряд ошибок. Нет — admin должен включить в UI.
413 Body > 256 KB. Нет — урезать payload.
429 Rate limit превышен. Заголовок Retry-After: 1. Да, через Retry-After секунд.
502 Внутренний сервис чата недоступен. Да, с exponential backoff.

Тело ответа при ошибках — generic error, без деталей (сделано осознанно: не хотим облегчать перебор подписей/токенов).


Push policy#

По умолчанию webhook-сообщения не триггерят push. Push срабатывает только при @mention: передан хотя бы один элемент в mentions[], либо в тексте есть @channel, @here.

Матрица поведения (webhook с default_push: true)#

Payload Что происходит
Нет mention в тексте и mentions: [] Только in-app уведомление, push — нет.
silent: true Только in-app уведомление. severity и mentions игнорируются для push-пути.
severity: "info" + есть mention Только in-app уведомление, push — нет.
severity: "warning" (default) + есть mention Push с учётом DND/mute/quiet hours получателя.
severity: "critical" + есть mention Push пробивает DND и quiet hours. muted_conversations всё равно уважаются — если юзер заглушил канал, не придёт.

Если у webhook'а default_push: false, все комбинации эквивалентны "только in-app уведомление". Исключение — payload с silent: false: это разовый override, push разрешён.

Спецтокены в тексте#

Токен Эффект
@channel push всем участникам канала (с учётом DND/mute/quiet-hours)
@here push только онлайн-сессиям без focus (Slack semantics)

Синонимы severity#

Приложения из экосистемы PagerDuty/Sentry/Opsgenie присылают свои уровни. Endpoint приводит их к трём внутренним:

Входит как Становится
error, fatal, emergency, alert, critical critical
warn, warning warning
debug, notice, info info
Что-либо другое warning (fallback)

Rate limits#

Тариф msg/sec per webhook
Free 5
Pro 10
Enterprise 30

При превышении — HTTP 429 + Retry-After: 1.


Auto-disable#

После 100 подряд ошибочных доставок (4xx или 5xx от endpoint'а) webhook автоматически отключается. Дальнейшие запросы возвращают HTTP 410 Gone. Admin включает webhook вручную в UI.

Счётчик сбрасывается на первом успешном 2xx.


Примеры#

curl — token-only#

curl -X POST "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d '{"text":"Hello from CI"}'

curl — с подписью (PartyFlow-native)#

SECRET='whsec_...'
BODY='{"text":"Hello signed"}'
TS=$(date +%s)
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
 
curl -X POST "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H "X-PartyFlow-Timestamp: $TS" \
  -H "X-PartyFlow-Signature: $SIG" \
  -d "$BODY"

Python#

import hmac, hashlib, time, json, requests
 
WEBHOOK_URL = "https://api.partyflow.ru/api/v1/webhooks/incoming/whk_..."
SECRET = b"whsec_..."
 
body_dict = {"text": "Hello from Python"}
body = json.dumps(body_dict, separators=(",", ":")).encode()  # stable serialization
ts = str(int(time.time()))
 
signing_string = ts.encode() + b"." + body
signature = "sha256=" + hmac.new(SECRET, signing_string, hashlib.sha256).hexdigest()
 
resp = requests.post(
    WEBHOOK_URL,
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-PartyFlow-Timestamp": ts,
        "X-PartyFlow-Signature": signature,
    },
)
resp.raise_for_status()
print(resp.json())

Node.js#

const crypto = require("crypto");
 
const WEBHOOK_URL = "https://api.partyflow.ru/api/v1/webhooks/incoming/whk_...";
const SECRET = "whsec_...";
 
const body = JSON.stringify({ text: "Hello from Node" });
const ts = Math.floor(Date.now() / 1000).toString();
 
const signature = "sha256=" + crypto
  .createHmac("sha256", SECRET)
  .update(`${ts}.${body}`)
  .digest("hex");
 
const resp = await fetch(WEBHOOK_URL, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-PartyFlow-Timestamp": ts,
    "X-PartyFlow-Signature": signature,
  },
  body,
});
 
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
console.log(await resp.json());

Связанное#