Skip to main content

Bridges API

Bridges connect channels to external services (webhooks, Microsoft Teams). See the External Bridges guide for conceptual background.

Config is write-only

The configJson field (containing OAuth tokens and HMAC secrets) is never returned by any API endpoint. Store secrets securely at the time of creation.


Channel bridge management

List bridges

GET /api/channels/:channelId/bridges

Auth: Required. Caller must be a channel member.

Response 200:

{
"bridges": [
{
"id": "br_01HZ...",
"channelId": "ch_01HZ...",
"externalService": "webhook",
"externalChannelId": "ci-alerts",
"externalChannelName": "CI Alerts",
"externalWorkspaceId": "acme-ci",
"syncDirection": "incoming",
"isSyncEnabled": true,
"createdAt": "2026-05-26T09:00:00Z"
}
]
}

Create a bridge

POST /api/channels/:channelId/bridges

Auth: Required. Caller must be a channel owner or global admin.

Webhook bridge

{
"externalService": "webhook",
"externalChannelId": "ci-alerts",
"externalChannelName": "CI Alert Webhook",
"externalWorkspaceId": "acme-ci",
"syncDirection": "incoming",
"config": {
"webhookUrl": "",
"outgoingUrl": "https://hooks.acme.com/vaultysclaw",
"secret": "a-random-secret-at-least-32-chars"
}
}

Teams bridge

{
"externalService": "teams",
"externalChannelId": "19:abc123@thread.tacv2",
"externalChannelName": "Engineering (Teams)",
"externalWorkspaceId": "tenant-id",
"syncDirection": "bidirectional",
"config": {
"accessToken": "eyJ...",
"tenantId": "tenant-id",
"botId": "bot-app-id"
}
}

Common fields:

FieldTypeRequiredDescription
externalService"webhook" | "teams"YesBridge type
externalChannelIdstringYesExternal identifier (webhook: your choice; Teams: channel ID)
externalChannelNamestringYesHuman-readable label for the UI
externalWorkspaceIdstringYesLogical grouping (CI platform name, tenant ID, etc.)
syncDirectionstringNo"incoming", "outgoing", or "bidirectional". Default: "bidirectional"
configobjectYesService-specific configuration (see above)

Webhook config fields:

FieldRequiredDescription
secretYesHMAC-SHA256 shared secret for incoming request verification
outgoingUrlNoURL the control plane POSTs to for outgoing messages
webhookUrlNoUnused; leave empty

Teams config fields:

FieldRequiredDescription
accessTokenYesMicrosoft Graph API OAuth access token
tenantIdYesAzure AD tenant ID
botIdYesTeams bot application (client) ID

Response 201:

{
"bridge": {
"id": "br_01HZ...",
"externalService": "webhook",
"syncDirection": "incoming",
"isSyncEnabled": true,
...
}
}

Error 400: Missing required fields or invalid externalService.

Error 409: A bridge for this (channelId, externalService, externalChannelId) combination already exists.


Update a bridge

PATCH /api/channels/:channelId/bridges/:bridgeId

Auth: Required. Caller must be a channel owner or global admin.

All fields are optional. Send only the fields you want to change.

{
"isSyncEnabled": false,
"syncDirection": "outgoing"
}
FieldTypeDescription
isSyncEnabledbooleanEnable or disable sync without deleting configuration
syncDirectionstringChange direction: "incoming", "outgoing", "bidirectional"

Response 200:

{
"bridge": {
"isSyncEnabled": false,
"syncDirection": "outgoing",
...
}
}

Error 404: Bridge not found.


Delete a bridge

DELETE /api/channels/:channelId/bridges/:bridgeId

Auth: Required. Caller must be a channel owner or global admin.

Permanently removes the bridge and its encrypted configuration. Any external service still pointed at the incoming URL will receive 404.

Response 200:

{ "success": true }

Incoming webhook endpoint

This endpoint is public (no session required) and is authenticated solely via HMAC-SHA256 signature.

POST /api/bridges/webhook/:bridgeId/incoming

Headers:

HeaderRequiredDescription
Content-TypeYesapplication/json
X-SignatureYessha256=<hex> — HMAC-SHA256 of the raw request body

Request body:

{
"message": "Build failed on main",
"author": "ci-bot",
"metadata": {
"buildId": "12345",
"repo": "acme/backend"
}
}
FieldRequiredDescription
messageYesContent posted to the channel. Must be non-empty.
authorNoDID or label. Defaults to webhook:external.
metadataNoArbitrary JSON stored in message metadata.

Response 200:

{
"ok": true,
"messageId": "msg_01HZ..."
}

Error responses:

StatusReason
400Missing or empty message field, or invalid JSON
401Invalid or missing X-Signature header
403Bridge syncDirection is "outgoing" only, or isSyncEnabled is false
404bridgeId does not exist, or bridge type is not webhook

Computing the HMAC signature

The signature is computed over the raw request body bytes (not re-serialised):

# Shell (curl + openssl)
BODY='{"message":"Build failed","author":"ci-bot"}'
SECRET="a-random-secret-at-least-32-chars"
SIG="sha256=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -X POST \
"https://vaultysclaw.acme.com/api/bridges/webhook/br_01HZ.../incoming" \
-H "Content-Type: application/json" \
-H "X-Signature: $SIG" \
-d "$BODY"
import hmac, hashlib, json, requests

secret = "a-random-secret-at-least-32-chars"
payload = json.dumps({"message": "Build failed", "author": "ci-bot"})
sig = "sha256=" + hmac.new(
secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()

requests.post(
"https://vaultysclaw.acme.com/api/bridges/webhook/br_01HZ.../incoming",
data=payload,
headers={"Content-Type": "application/json", "X-Signature": sig},
)
const crypto = require("crypto");

const secret = "a-random-secret-at-least-32-chars";
const payload = JSON.stringify({ message: "Build failed", author: "ci-bot" });
const sig =
"sha256=" + crypto.createHmac("sha256", secret).update(payload).digest("hex");

await fetch(
"https://vaultysclaw.acme.com/api/bridges/webhook/br_01HZ.../incoming",
{
method: "POST",
headers: { "Content-Type": "application/json", "X-Signature": sig },
body: payload,
}
);

Incoming Teams endpoint

POST /api/bridges/teams/incoming

This endpoint is called by the Microsoft Teams Bot Framework when a user posts in a Teams channel linked to VaultysClaw. It looks up the bridge by channelId from the Teams event payload and creates a message in the corresponding VaultysClaw channel.

Bot Framework authentication

JWT verification of the Bot Framework Authorization header is marked as a TODO in the current preview implementation. Do not expose this endpoint publicly without adding that verification.

Headers:

HeaderDescription
AuthorizationBearer token from Bot Framework (verification TODO)
Content-Typeapplication/json

The request body follows the Bot Framework Activity schema. The endpoint extracts activity.channelData.teamsChannelId, looks up the matching bridge, and creates a ChannelMessage from the activity text.

Response 200: { "ok": true, "messageId": "..." } on success.