A lightweight HTTP bridge that receives webhook payloads and forwards them as messages to a Matrix room, impersonating per-sender bot users via an Application Service token.
- A running Matrix homeserver (Synapse) with Application Service support
- Docker and Docker Compose
curl -fsSL https://raw.githubusercontent.com/krahlos/matrix-webhook-bridge/main/install.sh | sh
docker compose up -dSee INSTALL.md for full configuration reference and health checks. See MATRIX.md for registering the Application Service with Synapse.
Any HTTP client can post to /notify:
# generic message
curl -X POST "http://localhost:5001/notify" \
-H "Content-Type: application/json" \
-d '{"body": "Hello from the bridge!"}'
# with a registered service formatter
curl -X POST "http://localhost:5001/notify?service=alertmanager" \
-H "Content-Type: application/json" \
-d '{"body": "plain text", "html": "<b>bold text</b>"}'| Parameter | Description |
|---|---|
service |
Activates a built-in formatter and selects the sender via service_users in config |
room |
Sends to this Matrix room ID, overriding any server-side routing |
The sender (Matrix user localpart and token) is determined server-side: the service_users map
in bridge.yml maps each service name to its user localpart. If the service is not listed,
default_user is used.
By default all messages go to the global room_id in bridge.yml. You can route per service
by adding a service_rooms map under server::
server:
service_rooms:
alertmanager:
- "!abc123:matrix.example.org"
- "!def456:matrix.example.org"
borgmatic:
- "!abc123:matrix.example.org"Room resolution order (first match wins):
?room=<id>— message is sent to exactly this one room, ignoring any configservice_rooms[service]— message is sent to all rooms listed for the servicematrix.room_id— fallback, single room
When matrix.autojoin: true is set, the bridge joins every configured room at startup on behalf
of each bot user. This is useful after adding a new room to service_rooms — instead of manually
inviting each bot, the bridge handles it automatically.
matrix:
autojoin: trueThe bridge derives the set of (user, room) pairs from the config:
default_userjoinsmatrix.room_id- Each entry in
service_roomsis joined by the matchingservice_usersentry, ordefault_userif no explicit mapping exists
Joining a room the bot is already in is a no-op. A failed join is logged as an error but does not prevent the bridge from starting.
The bridge ships a built-in formatter for Alertmanager and ready-to-use notification scripts for
other tools in integrations/.
| Tool | Type | ?service= value |
Description |
|---|---|---|---|
| Prometheus Alertmanager | built-in | alertmanager |
Colour-coded alerts with severity, description and links |
| borgmatic | standalone script | — | Backup job success/failure notifications |
| CrowdSec alert | standalone script | — | Per-decision ban/unban alerts |
| CrowdSec summary | standalone script | — | Daily digest of top attackers and blocked IPs |
Prometheus metrics are exposed at GET /metrics on the same port as the bridge. No authentication
is required.
| Metric | Labels | Description |
|---|---|---|
bridge_requests_total |
service |
Every POST /notify received |
bridge_notify_success_total |
service |
Matrix send succeeded |
bridge_notify_failure_total |
service |
Matrix send failed |
bridge_invalid_payload_total |
service |
400 Bad Request (bad JSON/oversized) |
bridge_auth_failure_total |
— | 401 Unauthorized |
The service label is the ?service= query value, or "" for generic requests.
# prometheus.yml
scrape_configs:
- job_name: matrix-webhook-bridge
static_configs:
- targets: ["localhost:5001"]This is developed with agentic assistance -- there is no warranty of fitness for any purpose.
It's been guided by an engineer with a passion for clean code, nice CX and good docs, but it's still heavy in LLM output, so you may find some bugs.