Skip to content

Integrations & webhooks

Integrations let external software participate in a workspace: they receive signed webhook events, post chat messages through a bot API, and handle slash commands with deferred responses. An integration is a synthetic, non-human actor scoped to one workspace — it authenticates with its own knbi_… token, never a user credential, and its posts render in chat as a bot with an APP badge.

Integration tokens are the machine-to-machine story; a personal API key (knak_…) acts as you instead.

Management routes are workspace-scoped and require an owner or admin; they accept a session JWT or API key. The UI equivalent lives in Settings → Integrations — see the admin guide.

Endpoint Description
POST /v1/workspaces/{wid}/integrations Create → 201 {integration, token} — the knbi_… token appears here and never again
GET /v1/workspaces/{wid}/integrations List (no hashes or private keys)
GET /v1/workspaces/{wid}/integrations/{iid} One integration
PATCH /v1/workspaces/{wid}/integrations/{iid} Update name/description/webhook URL/subscriptions/commands, or enable/disable
DELETE /v1/workspaces/{wid}/integrations/{iid} Delete
POST /v1/workspaces/{wid}/integrations/{iid}/rotate Rotate the token — returned once; the old token dies immediately
POST /v1/workspaces/{wid}/integrations/{iid}/ping Send a signed ping envelope to the webhook URL (visible test)
GET /v1/workspaces/{wid}/integrations/{iid}/deliveries Delivery ledger, newest first — every attempt with status code and timing
POST /v1/workspaces/{wid}/integrations/{iid}/deliveries/{did}/redeliver Re-send a delivery: a fresh attempt reusing the original signed envelope

An integration carries scopes (chat:read, chat:write, commands), a webhookUrl, eventSubscriptions, and optional slash commands ({name, description, usageHint}).

Every event, ping, and command uses one JSON envelope, POSTed to your webhookUrl:

{
"id": "evt_7c2a…",
"type": "chat.message.created",
"workspaceId": "w_5d81…",
"timestamp": "2026-07-16T12:00:00Z",
"payload": { "channelId": "c_…", "messageId": "m_…", "fromUid": "u_…", "text": "" }
}

Event catalog (chat, v1): ping, chat.message.created, chat.channel.created, and command.invoked. Payloads carry full content so you never need a read-back call, and an integration never receives events for its own bot’s actions.

Each delivery is signed with the integration’s Ed25519 key:

Header Value
X-Knobs-Signature ed25519=<base64 sig> over the string "v1:" + timestamp + ":" + rawBody
X-Knobs-Timestamp RFC 3339 timestamp bound into the signature — reject stale ones
X-Knobs-Key-Id which key signed (kid)
X-Knobs-Event-Id stable across retries — use it for idempotency

Fetch the public keyset (unauthenticated) and verify the detached signature:

Endpoint Description
GET /v1/integrations/{iid}/keys Public Ed25519 keyset by kid — no auth required

Failed deliveries retry on a backoff of 1m → 5m → 30m → 2h (5 attempts total), then the ledger row is marked exhausted — never silently dropped. Admins can inspect every attempt in the delivery log and redeliver with a fresh retry budget; a redelivery reuses the original envelope byte-for-byte, so X-Knobs-Event-Id idempotency still holds.

Bot routes authenticate with the integration token (Authorization: Bearer knbi_…), are scoped to the integration’s own workspace, and gate on scopes. Every response carries X-RateLimit-Remaining and X-RateLimit-Reset; the budget is 60 requests/minute per integration and 1 message/second per channel.

Endpoint Scope Description
GET /v1/bot/self The integration’s identity and granted scopes
GET /v1/bot/channels chat:read List channels in the workspace
POST /v1/bot/channels/{cid}/messages chat:write Post as the bot; supports an Idempotency-Key header — a replay returns the original message with 200
POST /v1/bot/interactions/{iid}/respond commands Deferred slash-command reply (15-minute window)
Terminal window
curl -X POST https://api.knobs.io/v1/bot/channels/{cid}/messages \
-H "Authorization: Bearer knbi_..." \
-H "Idempotency-Key: 7f3a2b1c-…" \
-H "Content-Type: application/json" \
-d '{"text": "Deploy finished ✅"}'

There is no 3-second ack cliff — commands are async by design:

  1. A member runs /yourcommand some text in chat. The channel-scoped invoke route is documented with the rest of chat: see Chat.
  2. Your integration receives a command.invoked envelope containing an interactionId and the actor/channel context.
  3. Respond whenever ready — up to 15 minutes later — via POST /v1/bot/interactions/{iid}/respond with {"text": "…"}. The reply posts to the channel as your bot. After expiry the endpoint returns 410 and the interaction is marked expired.
  • Admin: Integrations — the Settings UI for everything above.
  • API keys — personal tokens that act as a user, not a bot.
  • Chat — the member-facing chat surface, including the command invoke route.