Перейти к содержимому
CalabДокументация

Вебхуки

Вебхук бота с подписью sha256 и вебхук доски с подписью v1 и событиями task.* — настройка, формат, проверка подписи, повторы.

В Calab два вида вебхуков:

Вебхук ботаВебхук доски
Кто настраиваетсам бот, своим токеномчеловек с MANAGE_BOARD на доске и MANAGE_INTEGRATIONS в пространстве
ТарифлюбойBusiness и выше
Что приходитсобытия сообщений, реакций, задач, встреч, кнопок, уведомлений — то же, что в gatewayкаждое изменение задач одной доски: task.*
Формат телаBotWebhookUpdate (protojson, lowerCamelCase)BoardWebhookEvent (protojson с именами полей proto, snake_case)
ПодписьX-Calab-Signature: sha256=<hex>X-Calab-Signature: v1=<hex> с меткой времени

Вебхук бота

Вместо gateway — или вместе с ним — сервер может присылать боту события по HTTPS. Удобно для serverless: боту не нужен постоянный процесс.

Настройка

curl -s -X PUT $CALAB/api/bots/me/webhook \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"url\": \"https://bot.example.com/calab\", \"secret\": \"$WEBHOOK_SECRET\"}"
  • url — только https:// на публичный адрес: частные сети, localhost и приватные диапазоны IP отклоняются.
  • secret — от 16 до 256 символов. Храните его отдельно от токена.
  • Ответ — { "webhook": { "url", "enabled", "disabledAt", "failingSince", "lastOkAt", "lastError", "pending" } }.
  • GET /api/bots/me/webhook — текущее состояние, DELETE → 204 (очередь сбрасывается).

Какие события приходят

  • messageCreate, messageUpdate, messageDelete, messageReactionAdd, messageReactionRemove — комнат, которые бот видит, и его DM. Кроме собственных сообщений и реакций бота. Команды — как в gateway, в message.command.
  • taskCreate, taskUpdate, taskDelete, taskActivity — досок, которые бот видит.
  • eventCreate, eventUpdate, eventDelete, eventRsvp — как в gateway: внешние адреса только боту, который может менять встречу.
  • workspaceMemberUpdate.
  • botCallback — нажатия кнопок под сообщениями бота (см. Команды и кнопки).
  • taskNotice — уведомления людей, выбравших этого бота (см. Уведомления о задачах).

События голоса (voiceStateUpdate и другие) приходят только через gateway.

Запрос

HTTP
POST /calab HTTP/1.1
Host: bot.example.com
Content-Type: application/json
User-Agent: CalabBot-Webhook/1.0
X-Calab-Delivery: 0192c4a0-…
X-Calab-Signature: sha256=5d2f…c41a
JSON
{
  "id": "0192c4a0-…",
  "botUserId": "0192a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
  "createdAt": "2026-10-14T10:05:00Z",
  "event": {
    "messageCreate": {
      "workspaceId": "0191f0c2-0000-7000-8000-00000000a001",
      "message": {
        "id": "0192c3f0-…",
        "roomId": "0191f0d4-0000-7000-8000-00000000b001",
        "authorId": "0191f0c2-7a10-7d3e-9b11-5c2f8e4a6d01",
        "content": "/ping",
        "command": { "botUserId": "0192a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b", "name": "ping", "args": "" }
      }
    }
  }
}

Тело — BotWebhookUpdate { id, botUserId, createdAt, event }, где event — тот же DispatchEvent, что в gateway.

Проверка подписи

X-Calab-Signature = sha256= + hex(HMAC-SHA256(secret, тело)). Считайте HMAC над сырыми байтами тела, а не над пересобранным JSON, и сравнивайте за постоянное время.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret, rawBody, header) {
  const want = Buffer.from('sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'));
  const got = Buffer.from(header ?? '');
  return want.length === got.length && timingSafeEqual(want, got);
}

В SDK: new Bot(token, { server, webhookSecret }) и bot.handleWebhook(rawBody, headers) — проверяет подпись, отбрасывает повторы и генерирует те же события message / command / reaction / callback, что и gateway. Отдельно — verifyWebhookSignature(secret, rawBody, header).

import { createServer } from 'node:http';
import { Bot } from '@calaba/bot-sdk';

const bot = new Bot(process.env.BOT_TOKEN, { server: 'https://app.example.com', webhookSecret: process.env.WEBHOOK_SECRET });
bot.on('command', (c) => bot.reply(c, 'pong'));

createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c)).on('end', () => {
    res.statusCode = bot.handleWebhook(Buffer.concat(chunks), req.headers) ? 204 : 401;
    res.end();
  });
}).listen(8080);

Доставка и повторы

  • Успех — любой 2xx за 10 секунд. Редиректы не выполняются и считаются ошибкой.
  • Повторы: через 1 мин, 2, 4 … до 1 часа между попытками; доставка живёт сутки.
  • Вебхук, который ошибается целые сутки, отключается: webhook.disabledAt, очередь сбрасывается, владелец бота и управляющие получают botUpdate. PUT включает его снова.
  • Порядок не гарантирован, повторы возможны — дедуплицируйте по id. Отвечайте быстро, работу делайте после ответа.

Вебхук доски

Доска отправляет JSON на ваш HTTPS-адрес при каждом изменении её задач. Это не вебхук бота: один вебхук на доску, все изменения задач, без фильтра событий. Подходит для синхронизации с трекерами, BI и CRM.

Маршруты

МетодПутьЧто делает
PUT/api/boards/{id}/webhookсоздать, заменить или включить отключённый: {url, secret?}
GET/api/boards/{id}/webhookсостояние: url, hasSecret, enabled, disabledAt, failingSince, lastOkAt, lastError, pending, pausedReason
DELETE/api/boards/{id}/webhookудалить → 204, очередь помечается failed
POST/api/boards/{id}/webhook/pingсинхронно отправить событие ping → {ok, status, error}; не чаще раза в 10 с (429)

url — только https:// на публичный адрес. secret — от 16 до 256 символов; пусто — сервер сгенерирует 32 случайных байта (base64url) и вернёт secret один раз в ответе. Потом секрет не возвращается никогда.

События

typeКогда
task.createdзадача создана
task.updatedлюбое изменение: поля, статус, исполнители, связи, вложения, согласование, чек-листы, вехи, Git-связи
task.archived · task.restoredзадача в архиве · восстановлена (автоархив — actor: null)
task.moved_inзадача пришла с другой доски (у неё уже новый ключ) — на доску-получатель
task.moved_outзадача ушла на другую доску — на доску-источник
task.comment.created · task.comment.updated · task.comment.deletedкомментарии
pingпроверочное событие

Изменения одной транзакции — одно событие. changes — записи журнала задачи: field — вид записи (status, assignees, checklist, milestones, git …), before / after — её данные. Изменения, сделанные правилом автоматизации, приходят отдельным событием той же транзакции с "actor": null и "rule": {"id", "name"}; у изменений людей и ботов rule — null. Git-события (связи задачи с ветками и pull request) — это task.updated с changes[field=git] и "actor": null.

Запрос

ЗаголовокЗначение
Content-Typeapplication/json
User-AgentCalab-Webhook/1.0
X-Calab-Webhook-Version1 — контракт только расширяется новыми полями
X-Calab-Eventtype события
X-Calab-Deliveryid доставки (= id в теле)
X-Calab-TimestampUnix-время отправки в секундах
X-Calab-Signaturev1=<hex HMAC-SHA256(secret, timestamp + "." + тело)>

Формат тела

BoardWebhookEvent в protojson, но с именами полей proto (snake_case), а не lowerCamelCase, как в REST. Все поля присутствуют: незаданные — null (actor: null для серверных изменений, edited_at: null), пустые строки и списки — как есть; uint64 (например, size вложения) — строка, sequence — число. task — задача без данных зрителя (subscribed, muted, unread, viewer_state — всегда по умолчанию), attachments и checklists всегда пусты (счётчики attachment_count, checklist_total / checklist_done остаются), вехи (milestones, milestone_progress, task_milestone_id) — как есть; comment — только у task.comment.*. Ссылка на задачу — task_url.

Пример (поля по алфавиту; чтобы показать и changes, и comment, они собраны вместе — в настоящем task.updated поле comment равно null):

JSON
{
  "actor": { "id": "0192a000-0000-7000-8000-0000000000cc", "is_bot": false, "name": "Анна" },
  "board": { "id": "0192a000-0000-7000-8000-0000000000bb", "key": "FNG", "name": "Финансы" },
  "changes": [
    {
      "after": { "status_id": "s2", "status_type": "started" },
      "before": { "status_id": "s1", "status_type": "unstarted" },
      "field": "status"
    }
  ],
  "comment": {
    "attachments": [{ "mime": "application/pdf", "name": "a.pdf", "size": "1024" }],
    "author_id": "u1",
    "created_at": "2026-10-02T12:00:00Z",
    "edited_at": null,
    "id": "m1",
    "text": "готово"
  },
  "id": "0192a000-0000-7000-8000-000000000001",
  "occurred_at": "2026-10-02T12:00:00Z",
  "rule": null,
  "sequence": 42,
  "task": {
    "approval_required": 0,
    "approval_state": "TASK_APPROVAL_STATE_UNSPECIFIED",
    "approvers": [],
    "archived_at": null,
    "assignees": [],
    "attachment_count": 0,
    "attachments": [],
    "board_id": "0192a000-0000-7000-8000-0000000000bb",
    "checklist_done": 3,
    "checklist_total": 7,
    "checklists": [],
    "comment_count": 0,
    "completed_at": null,
    "completed_by": "",
    "created_at": "2026-10-02T12:00:00Z",
    "created_by": "",
    "description": "",
    "due_on": "",
    "estimate": 3,
    "git_links": [],
    "git_links_count": 0,
    "id": "0192a000-0000-7000-8000-0000000000dd",
    "key": "FNG-12",
    "label_ids": [],
    "milestone_id": "",
    "milestone_progress": null,
    "milestones": [],
    "muted": false,
    "number": 12,
    "parent_id": "",
    "position": 0,
    "priority": "TASK_PRIORITY_HIGH",
    "relations": [],
    "room_id": "",
    "start_on": "",
    "started_at": null,
    "status_id": "s2",
    "subscribed": false,
    "subtask_count": 0,
    "subtask_done": 0,
    "task_milestone_id": "",
    "title": "Отчёт",
    "unread": false,
    "updated_at": "2026-10-02T12:00:00Z",
    "viewer_state": false,
    "workspace_id": ""
  },
  "task_url": "https://app.example.com/t/FNG-12",
  "type": "task.updated",
  "version": 1,
  "workspace_id": "0192a000-0000-7000-8000-0000000000aa"
}

task.moved_out несёт только то, что знала доска-источник: в task — лишь id, старый key и board_id источника, changes — одна запись moved_board с before. Задача целиком (новый ключ, статусы и метки получателя) приходит в task.moved_in доски-получателя.

Проверка подписи

HMAC считается над timestamp + "." + сырые байты тела. Отклоняйте запрос, если |сейчас − timestamp| > 5 минут (защита от повтора), и сравнивайте подписи за постоянное время.

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody — сырые байты тела (Buffer), headers — заголовки в нижнем регистре
export function verifyBoardWebhook(secret, rawBody, headers, now = Date.now() / 1000) {
  const ts = headers['x-calab-timestamp'];
  if (!ts || Math.abs(now - Number(ts)) > 300) return false; // окно ±5 минут
  const want = Buffer.from('v1=' + createHmac('sha256', secret).update(`${ts}.`).update(rawBody).digest('hex'));
  const got = Buffer.from(headers['x-calab-signature'] ?? '');
  return want.length === got.length && timingSafeEqual(want, got);
}

В SDK: verifyBoardWebhook(secret, timestamp, body, signature) проверяет подпись и окно ±5 минут, parseBoardWebhookEvent(body) разбирает тело.

Доставка

  • Как минимум один раз: повторы возможны, порядок прихода не гарантирован. Идемпотентность — по id (запоминайте увиденные), порядок — по sequence (монотонно на доску, с 1; у ping — 0).
  • Успех — любой 2xx за 10 секунд, редиректы — ошибка. Принимайте тела до 256 КБ.
  • Повторы: через 1 мин, 2, 4 … до 1 часа, доставка живёт сутки. Вебхук, который ошибается сутки подряд, отключается (enabled: false, очередь сбрасывается); PUT включает его снова.
  • Пауза по тарифу: когда тариф опускается ниже Business, вебхук сохраняется, но встаёт на паузу (pausedReason = PLAN) — новые события не копятся; после повышения тарифа доставка продолжается с новых событий.
  • Архивная доска доступна только на чтение и событий не порождает; уже накопленная очередь доставляется.