PartyFlow

Reference: Custom Slash Commands#

Позволяет боту зарегистрировать свои команды вида /deploy, /oncall, /report. Когда пользователь пишет их в канале, PartyFlow доставляет invocation владельцу команды одним из двух способов:

  • event_delivery_mode="poll": кладёт событие slash.command в GET /api/v1/bot/events для owning-бота.
  • event_delivery_mode!="poll": отправляет HMAC-подписанный POST на callback_url, ждёт ответ до 3 секунд и публикует результат в чат.

Если бот не может ответить сразу — он позже досылает ответ через trigger_id (аналог Slack response_url, TTL 5 минут, single-use).


Base URL#

https://api.partyflow.ru

Во всех примерах — переменная $BASE.


Authentication#

Все CRUD-запросы к slash command API используют тот же Bearer token, что и Bot REST API:

Authorization: Bearer fri_bot_<token>

Токен привязан к боту. Через токен бот может управлять только своими командами — попытка POST/DELETE с token от другого бота возвращает 403 Forbidden.


Registration flow#

  1. Бот вызывает POST /api/v1/bots/{bot_id}/commands — PartyFlow в ответе единоразово возвращает signing_secret.
  2. Бот сохраняет signing_secret (больше PartyFlow его не покажет).
  3. Пользователь в UI пишет /deploy prod.
  4. Если бот сейчас в poll mode, PartyFlow кладёт slash.command в poll queue и сразу показывает пользователю ephemeral ack «Команда принята, ожидаю ответ бота.»
  5. Если бот не в poll mode, PartyFlow шлёт HMAC-подписанный POST на callback_url.
  6. Бот отвечает JSON (sync callback) или позже отправляет POST /api/v1/bot/messages + заголовок X-PartyFlow-Trigger-Id.

Endpoints#

POST /api/v1/bots/{bot_id}/commands#

Регистрирует новую команду.

Request body:

{
  "name": "deploy",
  "description": "Deploy service to environment",
  "usage_hint": "[env]",
  "callback_url": "https://your-bot.example.com/partyflow/slash"
}
Поле Тип Обязательно Ограничения
name string Да 1..32 символа, нижний регистр, ^[a-z][a-z0-9_-]*$. Без ведущего слэша.
description string Нет до 200 символов. Показывается в подсказке slash command UI.
usage_hint string Нет до 100 символов. Пример параметров ("[env] [service]").
callback_url string Условно HTTPS URL на инфраструктуре бота. Обязательно для не-poll ботов. Для poll-бота можно передать пустую строку или не передавать поле. SSRF-валидируется при регистрации и при каждом callback dispatch.

Response (201 Created):

{
  "ok": true,
  "command": {
    "id": "9a2f...",
    "space_id": "…",
    "bot_id": "…",
    "name": "deploy",
    "description": "…",
    "usage_hint": "[env]",
    "callback_url": "https://your-bot.example.com/partyflow/slash",
    "is_active": true,
    "created_at": "2026-04-18T10:00:00Z",
    "updated_at": "2026-04-18T10:00:00Z"
  },
  "signing_secret": "dead...beef"
}

Important: signing_secret возвращается один раз. Сохраните его сразу — повторного запроса не будет. Для ротации удалите команду и создайте заново.

Errors:

Статус Причина
400 Неверный JSON / name не подходит под регекс / description или usage_hint длиннее лимита / callback_url не HTTPS / callback_url пустой у не-poll бота.
403 Bearer token не соответствует bot_id в URL (cross-bot попытка).
409 Имя уже зарегистрировано в space этого бота или зарезервировано (poll, remind).
402 Превышен кап на количество команд (25 на бота, 100 на space).
500 Внутренняя ошибка (детали в логах оператора).

GET /api/v1/bots/{bot_id}/commands#

Список активных команд бота.

Response (200):

{
  "ok": true,
  "commands": [
    {
      "id": "…",
      "name": "deploy",
      "description": "…",
      "usage_hint": "[env]",
      "callback_url": "https://your-bot.example.com/partyflow/slash",
      "is_active": true,
      "created_at": "…",
      "updated_at": "…"
    }
  ]
}

signing_secret не возвращается. Если потеряли — пересоздайте команду. У poll-команд callback_url может быть пустой строкой.


DELETE /api/v1/bots/{bot_id}/commands/{name}#

Удаляет команду. Возвращает 200 при успехе, 404 если команды нет.


Dispatch contract#

Delivery mode выбирается по текущему event_delivery_mode бота-владельца в момент invocation. Если бот сейчас в poll, используется poll delivery даже если у команды заполнен callback_url.

event_types[] не участвует: custom slash всегда доставляется owning-боту команды.

Poll delivery#

Poll-бот получает событие через GET /api/v1/bot/events:

{
  "event_type": "slash.command",
  "event_id": "8b4c7f2a-...",
  "payload_json": "{\"trigger_id\":\"8b4c7f2a-...\",\"command\":\"deploy\",\"command_id\":\"9a2f...\",\"space_id\":\"...\",\"conversation_id\":\"...\",\"user_id\":\"...\",\"text\":\"prod\",\"raw_text\":\"/deploy prod\",\"invoked_at\":\"2026-04-18T10:12:45.123456789Z\"}"
}

event_id равен trigger_id, поэтому бот может использовать его для idempotency. Пользователь сразу получает ephemeral ack; финальный результат бот отправляет async-ответом через X-PartyFlow-Trigger-Id.

Callback delivery#

Не-poll бот получает HTTP запрос на callback_url:

POST https://your-bot.example.com/partyflow/slash
Content-Type: application/json
User-Agent: PartyFlow-Webhooks/1.0
X-PartyFlow-Signature: sha256=<hex>
X-PartyFlow-Timestamp: 1713434400
X-PartyFlow-Delivery-Id: 8b4c7f2a-...     // same as trigger_id
X-PartyFlow-Trigger-Id: 8b4c7f2a-...       // use for async response
X-PartyFlow-Command: deploy
X-PartyFlow-Event: slash_command
X-PartyFlow-Webhook-Version: 1

Body:

{
  "trigger_id": "8b4c7f2a-…",
  "command": "deploy",
  "command_id": "9a2f…",
  "space_id": "…",
  "conversation_id": "…",
  "user_id": "…",
  "text": "prod",
  "raw_text": "/deploy prod",
  "invoked_at": "2026-04-18T10:12:45.123456789Z",
  "thread_id": "…"
}

text — аргументы после имени команды (всё, что шло после первого пробела). thread_id присутствует только если команда вызвана в треде.


Response modes#

Sync response (бот отвечает в течение 3 секунд)#

Ответ — JSON с полями:

Поле Значения Эффект
response_type "in_channel" / "ephemeral" (default) in_channel — сообщение видит весь канал. ephemeral — только вызвавший пользователь.
text string Текст сообщения. Обязательное поле для непустого ответа.

Пример sync (публично в канал):

{"response_type": "in_channel", "text": "Deploy to prod started: https://ci.example.com/run/42"}

Пример sync (видимое только пользователю):

{"response_type": "ephemeral", "text": "Unknown environment `prod2`. Try `prod`, `staging`."}

Async response (долгая операция)#

В callback mode, если операция занимает больше 3 секунд — ответьте пустым body и кодом 200. Пользователь получит эпемерное «Команда принята». В poll mode этот ack уже отправлен при постановке события в очередь. Финальный результат в обоих режимах отправляется через:

POST $BASE/api/v1/bot/messages
Authorization: Bearer fri_bot_<token>
Content-Type: application/json
X-PartyFlow-Trigger-Id: <trigger_id из исходного dispatch>
 
{
  "content": "Deploy finished in 4m 12s",
  "metadata_json": "{\"version\":1,\"blocks\":[...]}"
}

metadata_json необязателен. Если в нём есть interactive blocks, PartyFlow подпишет кнопки/selects так же, как при обычном POST /api/v1/bot/messages.

PartyFlow валидирует:

  • trigger_id существует и не использован (single-use, TTL 5 минут).
  • Bearer token принадлежит тому же боту, что получил исходный dispatch.

Канал, тред, space берутся из сохранённого контекста триггера — не из тела запроса. Это защита от confused-deputy.

Async errors:

Статус Причина
400 Отсутствует content.
403 Trigger принадлежит другому боту.
410 Trigger истёк (TTL 5 мин) или уже использован.

HMAC verification#

HMAC-заголовки используются только в callback delivery. Poll delivery защищается Bearer bot token'ом на GET /api/v1/bot/events.

Схема: v1:{timestamp}:{raw_body} → HMAC-SHA256 → hex → X-PartyFlow-Signature: sha256=<hex>.

Идентично outgoing webhooks — см. Security и verify-signatures.

Python:

import hmac, hashlib, time
 
def verify_slash(secret, timestamp, body, signature):
    # Protect against replay: accept requests within ±5 min
    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)

Go:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strconv"
    "time"
)
 
func VerifySlash(secret, timestamp string, body []byte, signature string) bool {
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil || abs(time.Now().Unix()-ts) > 300 {
        return false
    }
    base := fmt.Sprintf("v1:%s:%s", timestamp, string(body))
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(base))
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Rate limits#

Уровень Лимит Поведение
per-user × command 30 req/min Превышение → пользователь получает эпемерный «Too many requests».
per-command global 300 req/min Защита endpoint бота от stampede.

Лимиты зашиты на стороне PartyFlow — вашему боту не нужно считать их самому.


Caps (registration)#

Уровень Cap (default)
Команд на бота 25
Команд на space 100

Built-in команды (/poll, /remind) не учитываются в лимите space.


Best practices#

  1. Отвечайте быстро. Если операция дольше 3 секунд — верните 200 "" и дошлите через trigger_id.
  2. Валидируйте HMAC на каждом запросе. Без проверки подписи любой злоумышленник может вызвать ваш endpoint.
  3. Храните signing_secret в secret manager (Vault / AWS Secrets Manager / GCP Secret Manager). Никогда не коммитите в Git.
  4. Не читайте channel_id / space_id из async запроса к себе — PartyFlow берёт их только из сохранённого trigger контекста.
  5. Отвечайте ephemeral, когда это ошибка пользователя (неверные аргументы и т.п.) — весь канал не должен видеть эти сообщения.
  6. Используйте usage_hint для автокомплита в UI — помогает пользователям попадать в правильный синтаксис.

Curl quickstart#

# 1. Регистрация команды /deploy
curl -X POST "$BASE/api/v1/bots/$BOT_ID/commands" \
  -H "Authorization: Bearer $BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "deploy",
    "description": "Deploy service to environment",
    "usage_hint": "[env]",
    "callback_url": "https://your-bot.example.com/partyflow/slash"
  }'
# → сохраните signing_secret из ответа
 
# 2. Листинг команд
curl -s "$BASE/api/v1/bots/$BOT_ID/commands" \
  -H "Authorization: Bearer $BOT_TOKEN"
 
# 3. Удаление команды
curl -X DELETE "$BASE/api/v1/bots/$BOT_ID/commands/deploy" \
  -H "Authorization: Bearer $BOT_TOKEN"
 
# 4. Async отправка (после получения trigger_id от PartyFlow)
curl -X POST "$BASE/api/v1/bot/messages" \
  -H "Authorization: Bearer $BOT_TOKEN" \
  -H "X-PartyFlow-Trigger-Id: 8b4c7f2a-..." \
  -H "Content-Type: application/json" \
  -d '{"content": "Deploy finished in 4m 12s"}'