---
audience: developer
summary: Wire an external chat system (Slack, Discord, your own) so its messages flow
  into a ScaiWave room.
title: Build an integration bridge
path: tutorials/developer/build-an-integration-bridge
status: published
---

# Build an integration bridge

A bridge is a mirror between a ScaiWave room and a channel/room on
another system. ScaiWave handles ScaiWave→foreign relay; you handle
the foreign→ScaiWave webhook.

## 1. Plan the mapping

Decide:

- Which **direction**: inbound only, outbound only, or both?
- Which **ScaiWave room** ↔ which **foreign channel**?
- How are foreign user identities represented? (Most platforms have
  a user id; you'll use it to dedupe shadow participants.)
- What's the **HMAC** scheme on the foreign side? (We'll use that
  for both directions.)

## 2. Create the bridge

```bash
curl -X POST "$BASE/v1/admin/bridges" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "transport": "custom",
    "direction": "both",
    "room_id": "<scaiwave-room-id>",
    "name": "Acme CRM bridge",
    "outbound_webhook": "https://crm.acme.com/scaiwave/incoming"
  }'
```

Response includes `bridge_id` and `shared_secret` (the HMAC key).
**Store the secret somewhere safe** — it's shown once. Both sides
will sign with it.

## 3. Configure the foreign side

The foreign side needs to know:

- The shared secret (for signing incoming webhooks to ScaiWave and
  verifying outgoing ones).
- The ScaiWave inbound URL: `<host>/v1/bridges/<bridge_id>/inbound`.

When the foreign side has a new message, POST to that URL with:

```http
Content-Type: application/json
X-ScaiWave-Bridge-Signature: t=<unix-ts>,v1=<hex-hmac-sha256>
```

The signature is `HMAC-SHA256(shared_secret, f"{ts}.{body}")` —
the same Stripe-style scheme.

Body:

```jsonc
{
  "event_id": "<unique-id-from-foreign-side>",
  "sender": {
    "external_id": "U12345",
    "display_name": "Alice Anderson",
    "avatar_url": "https://crm.acme.com/avatar/U12345.png"
  },
  "content": {
    "msgtype": "swp.text",
    "body": "Hello from Acme CRM"
  },
  "thread_id": null,
  "reply_to_external_id": null
}
```

`event_id` is for idempotency — ScaiWave dedupes within a 10-minute
window. `sender.external_id` is what you map to a shadow
participant on the ScaiWave side; ScaiWave creates one
automatically if needed.

## 4. ScaiWave creates the event

When the inbound webhook arrives:

1. ScaiWave verifies the HMAC. Mismatch → 401, logged.
2. Looks up or creates a `bridge` shadow participant for
   `sender.external_id`.
3. Creates an event in the bridge's room with `sender_id =
   shadow.id`. The original sender info is embedded in
   `event.content._imported_sender`.
4. The event flows out over WebSocket like any other.

## 5. Outbound (ScaiWave → foreign)

Every event in the bridged room is routed through the
`relay_message_to_bridges` ARQ worker. It:

1. Looks up the bridge config.
2. Builds a JSON payload (similar shape to inbound).
3. Signs with the same shared secret.
4. POSTs to `bridge.outbound_webhook`.
5. Retries with exponential backoff on 5xx.

Your endpoint receives:

```json
{
  "event_id": "<scaiwave-event-id>",
  "sender": {
    "fqid": "@alice:scaiwave.example.com",
    "display_name": "Alice"
  },
  "content": {
    "msgtype": "swp.text",
    "body": "Reply from ScaiWave"
  },
  "ts": "2026-05-17T12:00:00Z"
}
```

Verify the HMAC the same way; if it doesn't match, return 401.
Otherwise post into your channel.

## 6. Slack / Discord / Teams (built-in transports)

These don't need a custom webhook endpoint on your side; ScaiWave's
relay knows their APIs.

For Slack:

```json
{
  "transport": "slack",
  "direction": "both",
  "room_id": "<scaiwave-room-id>",
  "slack_team_id": "T0123",
  "slack_channel_id": "C0123",
  "slack_signing_secret": "<slack-signing-secret>",
  "slack_bot_token": "xoxb-…"
}
```

ScaiWave subscribes to Slack's Events API at
`/v1/bridges/<id>/slack/events` and relays outbound via
`chat.postMessage`. The Slack signing secret lives on your side and
in ScaiWave; the bot token is needed for the outbound surface.

## 7. Operational

- **Pause** a bridge: `POST /v1/admin/bridges/{id}/pause`. Useful
  during incident response or maintenance.
- **Rotate the secret**: `POST /v1/admin/bridges/{id}/rotate-secret`.
  The old secret continues to work for 5 minutes for in-flight
  messages.
- **Delete**: `DELETE /v1/admin/bridges/{id}`. The bridge's
  history of shadow participants and events remains; future flow
  stops.

## 8. Error handling

- **HMAC mismatch (inbound)** → 401, logged for admin review.
- **Foreign side rate-limited (429)** → exponential backoff,
  retries 3 times. After that the message is marked
  `delivery_failed` in the audit log.
- **Foreign side unreachable** → retries up to 1 hour, then alert
  the admin.

## Where to go next

- API: [Bridges](/docs/scaiwave/reference/api/bridges).
- [Concepts: Bridges](/docs/scaiwave/concepts/bridges) vs.
  [Federation](/docs/scaiwave/concepts/federation).
