Download OpenAPI specification:
The HTTP protocol between Muster and a bridge to a messenger Muster has no native support for.
A bridge is a separate service that lets Muster deliver Alert Groups to a messenger it does not support natively (ADR-0017). Muster calls the bridge to post, edit and reply; the bridge calls its messenger, and reports button presses, Thread messages, Account link requests and deleted messages back to Muster. This document is the contract a bridge implements. Terms follow Muster's glossary: a Root message is the one editable message that shows an Alert Group in a channel, its Thread holds the replies under it, a Mention is a deliberate @-reference.
Version. Every path is under /v1/; GET /v1/info returns protocol_version "1". New optional fields and
features keep version 1, so a bridge ignores fields it does not know and Muster ignores fields of info it does not
know. A change that breaks existing bridges gets a new major path.
Authentication. Muster sends the Connection's token on every request as Authorization: Bearer <token>; the
bridge answers 401 to a missing or wrong token. Events that the bridge pushes to Muster are signed as Standard
Webhooks does (see webhooks). Muster sends the token only to the base URL saved on the Connection, follows no
redirects, and treats a redirect as a configuration error.
One token, one bot. Each Bridge Connection in Muster has a token of its own. A bridge that serves several
Connections — several bots, or several Muster installations — tells them apart by the token and keeps their events
apart: pollEvents returns, and the push sends to the callback configured for that token, only the events about
the messages posted with it and the messages people send to its bot. Muster ignores, without answering, a press or a
Thread message about a message it did not post through that Connection.
Ids in paths. message_id and channel_id are opaque strings of up to 512 characters; Muster percent-encodes
each as one path segment, and the bridge decodes it.
What every bridge implements. getInfo, postMessage, editMessage, replyInThread, checkChannel,
answerEvent, and inbound events through pollEvents or the bridgeEvent push, or both — at least account_link
events, because every bridge takes Account link tokens. Everything else is optional and declared in
info.capabilities: Muster never calls an operation the bridge did not declare and never sends a button to a bridge
without buttons. A bridge sends press events only with buttons, thread_message events only with
thread_messages and message_deleted events only with deletion_events; Muster handles every event type of this
protocol whatever its copy of info says, so an event sent just after a feature was switched on is not lost.
Rendering. Muster sends a JSON envelope and the bridge renders it in its messenger's markup. The body is
CommonMark 0.31 with one extension, strikethrough (~~text~~) as GitHub Flavored Markdown writes it. Muster uses
paragraphs, hard line breaks, emphasis, strong emphasis, strikethrough, code spans, links, bullet lists and block
quotes; never headings, tables, images or raw HTML. Alert data in the body is already escaped for CommonMark and
cannot mention anyone. Links are http(s) links, plus Mention markers: a link [@Display Name](muster-mention:<ref>)
whose ref names an entry of mentions. A bridge with native_mentions renders the marker as its messenger's
mention of that account; any other bridge renders the link text alone, as plain text. Muster keeps the body within
limits.max_body_length code points; the bridge makes the final cut where its rendered markup needs it — never inside
an entity, a tag or a surrogate pair — and reports it with truncated.
Idempotency. Every request that creates something carries an Idempotency-Key header. The bridge stores each key
per token — the same key sent with another Connection's token is another request — with its successful (2xx)
result for at least 24 hours and answers a repeated key with that first result and replayed: true, whatever the
body of the repeated request, without creating anything again; a failed request stores nothing, so its retry is a
new attempt. A repeated key whose first request is still in progress is answered transient. Where the messenger
has an idempotency mechanism of its own, the bridge passes the key on. Muster reuses a key when it retries a request,
also after a crash or a timeout; because a retry may carry a newer state than the first request, Muster follows a
replayed postMessage with an editMessage of its current state.
Errors are RFC 9457 problems (application/problem+json) with a class member that the bridge chooses, because
only the bridge knows what its messenger's codes mean:
class |
Means | Muster |
|---|---|---|
transient |
a temporary failure: the messenger is down, a timeout | retries with exponential backoff; repeated failures make the Destination Broken |
rate_limited |
the messenger or the bridge throttles; send Retry-After in seconds |
waits exactly that long, for the channel, or for every channel of the bot when scope is bot; not counted as a failure; a wait longer than 15 minutes, or none given, is treated as transient |
gone |
the message no longer exists — deleted, or its Thread is gone | publishes the Alert Group again once; with reason edit_window_closed, treats the message as past its edit window |
forbidden |
the channel is missing, the bot was removed from it or lacks a right, the bot was blocked | marks the Destination Broken until a check succeeds |
invalid |
the request cannot succeed as sent: too long, malformed, an unknown id | ends that delivery as Not delivered, without retrying |
A missing channel is forbidden, never gone. A problem without a class, or an answer that is not a problem, is
classified by its status: 401 and 403 as forbidden; 408 as transient; 429, and 503 with Retry-After,
as rate_limited; other 5xx as transient; 404, 410, a redirect and everything else as invalid. Timeouts
and network errors are transient. Muster never treats a message as gone because of a status code: a bridge that
means gone says so in class.
Presses are confirmed to the messenger by the bridge itself, within the messenger's deadline and at most 3
seconds after the press, before Muster has seen it; the bridge then reports the press as an event. Muster answers the
person, when it has something to say, with a separate answerEvent call, made before it acknowledges the event —
before it answers the push with 204 or polls with the next cursor.
Muster calls it on every Connection check, and again before a call when its copy is older than a few minutes. A bridge whose features change — buttons switched on, a new limit — is picked up without a restart of Muster.
{- "protocol_version": "1",
- "bridge": {
- "name": "string",
- "version": "string"
}, - "messenger": {
- "name": "string"
}, - "capabilities": {
- "buttons": {
- "max_per_message": 2,
- "max_per_row": 1,
- "data_max_bytes": 64,
- "styles": [
- "primary"
]
}, - "thread_messages": "all",
- "direct_messages": {
- "bot_can_start": true
}, - "private_answers": [
- "ephemeral"
], - "native_mentions": true,
- "quiet_messages": true,
- "status_color": true,
- "delete": {
- "max_age_seconds": 1
}, - "deletion_events": true
}, - "limits": {
- "max_body_length": 500,
- "edit_max_age_seconds": 300,
- "send_per_second_per_channel": 1,
- "send_per_second": 1
}, - "user_lookup": "email",
- "event_delivery": [
- "poll"
], - "link_url_template": "string"
}Posts a Root message, a Storm summary or a test message. notify: true is a Loud message: it should notify people as the messenger's new messages do, and carries the envelope's Mentions. A Quiet message (notify: false) is sent without a notification where the bridge declares quiet_messages, and otherwise as an ordinary message without Mentions. The answer names the message for later edits and Thread replies; it may also return the thread_ref of its Thread when the messenger's Thread id is known at once. A replayed answer is followed by an editMessage of Muster's current state.
| Idempotency-Key required | string^[!-~]{1,255}$ Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token. |
| status required | string (Status) Enum: "firing" "acknowledged" "snoozed" "resolved" "storm" The status of the Alert Group the message shows; |
| color required | string^#[0-9a-f]{6}$ The colour of the status, for messengers that colour messages. |
| emoji required | string [ 1 .. 8 ] characters The status as an emoji (🔴 🟠 ⚪ 🟢 ⛈), for messengers without colours. |
| title required | string [ 1 .. 512 ] characters
|
required | object (Link) |
| body required | string CommonMark, as the description of this protocol says; at most |
required | Array of objects (Button) In order. Empty when the message has none, always empty without |
| channel_id required | string [ 1 .. 512 ] characters |
| kind required | string (MessageKind) Enum: "root" "storm_summary" "test" A Root message of an Alert Group, a Storm summary, or a test message sent from the Destination page. |
required | Array of objects (Mention) |
| notify required | boolean Loud ( |
{- "channel_id": "ch-alerts",
- "kind": "root",
- "status": "firing",
- "color": "#d32f2f",
- "emoji": "🔴",
- "title": "#42 HighErrorRate",
- "link": {
- "label": "Open in Muster"
}, - "body": "[@Alice Smith](muster-mention:m1)\n\nprod · started 14:03\n\n> **namespace** payments\n\n- `api-7f9c` error rate 12 %\n- `api-5d2a` error rate 9 %\n\n[Runbook](https://wiki.example.org/runbooks/high-error-rate)",
- "buttons": [
- {
- "label": "Ack",
- "style": "primary",
- "data": "a1.k2.QmFzZTY0U2lnbmF0dXJlRXhhbXBsZQ"
}, - {
- "label": "Resolve",
- "style": "success",
- "data": "a2.k2.U2Vjb25kU2lnbmF0dXJlRXhhbXBsZQ"
}
], - "mentions": [
- {
- "ref": "m1",
- "external_id": "u-alice",
- "display_name": "Alice Smith"
}
], - "notify": true
}{- "message_id": "string",
- "thread_ref": "string",
- "truncated": true,
- "replayed": true
}Replaces the whole message — text, colour and buttons — with the envelope; an empty buttons removes them. Edits are always Quiet and never carry Mentions, so the envelope has neither. Editing to the state the message already shows is a success. A message that no longer exists is gone; an edit that the messenger refuses because the message is older than it allows is gone with the reason edit_window_closed. Muster stops editing a message once it is older than limits.edit_max_age_seconds, less a safety margin.
| message_id required | string [ 1 .. 512 ] characters The |
| status required | string (Status) Enum: "firing" "acknowledged" "snoozed" "resolved" "storm" The status of the Alert Group the message shows; |
| color required | string^#[0-9a-f]{6}$ The colour of the status, for messengers that colour messages. |
| emoji required | string [ 1 .. 8 ] characters The status as an emoji (🔴 🟠 ⚪ 🟢 ⛈), for messengers without colours. |
| title required | string [ 1 .. 512 ] characters
|
required | object (Link) |
| body required | string CommonMark, as the description of this protocol says; at most |
required | Array of objects (Button) In order. Empty when the message has none, always empty without |
{- "status": "firing",
- "color": "string",
- "emoji": "string",
- "title": "string",
- "body": "string",
- "buttons": [
- {
- "label": "string",
- "style": "primary",
- "data": "string"
}
]
}{- "truncated": true
}Offered when capabilities.delete is declared. Muster L1 never deletes messages; the operation is reserved so that a later version of Muster can remove its test messages without a new protocol version. A message that no longer exists is a success.
| message_id required | string [ 1 .. 512 ] characters The |
{- "title": "Unauthorized",
- "status": 401,
- "class": "forbidden"
}Posts a reply in the Thread of the message message_id, usually a Root message. Muster sends the last thread_ref it received for that message — from postMessage or from an earlier reply — or null when it has none: the bridge then creates the Thread if its messenger needs that. It returns a thread_ref, which Muster stores and sends with the next reply of the same Thread, so the bridge needs no store of its own that maps messages to Threads. A thread_ref is opaque to Muster; it may equal the message id. A Thread that no longer exists, or a message that was deleted, is gone. Replies may carry buttons, such as "Still on it" on a Reminder, when the bridge declares buttons.
| message_id required | string [ 1 .. 512 ] characters The |
| Idempotency-Key required | string^[!-~]{1,255}$ Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token. |
| thread_ref required | string or null <= 512 characters The last |
| body required | string CommonMark, at most |
required | Array of objects (Button) |
required | Array of objects (Mention) |
| notify required | boolean Loud ( |
{- "thread_ref": "string",
- "body": "string",
- "buttons": [
- {
- "label": "string",
- "style": "primary",
- "data": "string"
}
], - "mentions": [
- {
- "ref": "string",
- "external_id": "string",
- "display_name": "string"
}
], - "notify": true
}{- "message_id": "string",
- "thread_ref": "string",
- "truncated": true,
- "replayed": true
}The Destination check. It must not post anything. Answer 200 only when the bot can post, edit its own messages and reply in Threads there; answer forbidden with a detail a person can act on — "The bot is not a member of this channel." — when the channel does not exist or the bot cannot use it. Muster runs it when a Destination is saved, when a person presses "Check", and every few minutes while the Destination is Broken.
| channel_id required | string [ 1 .. 512 ] characters The channel as the messenger names it and as the Admin entered it on the Bridge Destination. |
{- "channel_id": "string",
- "name": "string"
}Offered when user_lookup is not none. Muster calls it when a person asks to link an account by a code and enters their handle — an email address, a login, a username or a user id, as user_lookup declares. The answer lists the one account with that handle, or none. A handle the bridge cannot search for is invalid.
| handle required | string [ 1 .. 320 ] characters The handle as the person entered it, in the kind |
{- "items": [
- {
- "external_id": "string",
- "display_name": "string",
- "handle": "string"
}
]
}Offered when capabilities.direct_messages is declared with bot_can_start. Muster uses it to send the code of an Account link to the account a person named in their Muster profile. The bridge opens the direct chat if needed. An account that cannot receive a message from the bot is forbidden.
| Idempotency-Key required | string^[!-~]{1,255}$ Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token. |
| user_external_id required | string [ 1 .. 512 ] characters |
| body required | string [ 1 .. 2000 ] characters CommonMark. |
{- "user_external_id": "string",
- "body": "string"
}{- "message_id": "string",
- "replayed": true
}Muster's answer to a press, a Text command or an Account link request: a refusal, "Done: Acknowledged #42", "Linked to Muster user …". The bridge shows text to the person who caused the event event_id and says where: an answer to a press privately when its messenger allows that (private_answers) and the press is recent enough, otherwise in the Thread of the message; an answer to a thread_message in that Thread, never as a pop-up; an answer to an account_link in the direct chat it came from. Muster calls it within a few seconds of receiving the event and before acknowledging it, or not at all; an event the bridge no longer knows, or can no longer answer anywhere, is gone.
| Idempotency-Key required | string^[!-~]{1,255}$ Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token. |
| event_id required | string^[!-~]{1,200}$ |
| text required | string [ 1 .. 500 ] characters Plain text; a pop-up may shorten it. |
{- "event_id": "string",
- "text": "string"
}{- "shown_as": "ephemeral",
- "replayed": true
}Presses, Thread messages, Account link requests and deleted messages, polled by Muster or pushed to it.
Offered when event_delivery contains push and chosen on the Connection. The bridge posts each event, one per
request, to the callback address that the Bridge Connection shows in Muster:
<MUSTER_INGEST_URL>/api/v1/callbacks/bridge/<connection id>.
The request is signed as Standard Webhooks specifies, with the Connection's push secret (whsec_ followed by the
base64 of at least 24 random bytes, configured in both Muster and the bridge): webhook-id is the event's id,
webhook-timestamp the Unix time of the attempt in seconds, and webhook-signature is v1, followed by the
base64 of HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<body> with the decoded secret. Muster refuses a
timestamp more than 5 minutes away from its own clock.
Muster answers 204 once it has handled the event, and also for an event id it has handled before. 400 means
the event can never be handled — the bridge drops it. 401 means the signature, the Connection or its mode is
wrong; the bridge keeps the event and retries after fixing its configuration. On any other answer or a timeout
the bridge retries with backoff, for at least one hour, keeping the same webhook-id.
| webhook-id required | string^[!-~]{1,200}$ The event's |
| webhook-timestamp required | string^[0-9]{1,12}$ Unix time of this attempt, in seconds. |
| webhook-signature required | string >= 4 characters
|
| id required | string^[!-~]{1,200}$ Stable across retries and polls; Muster handles each id once. Printable ASCII without spaces, so that it can travel as the |
| type required | string Value: "press" |
| occurred_at required | string <date-time> When it happened in the messenger, or when the bridge learned of it. |
| message_id required | string The message the button belongs to, a Root message or a Thread reply. |
required | object (Person) |
| data required | string The button's |
{- "id": "string",
- "type": "press",
- "occurred_at": "2019-08-24T14:15:22Z",
- "message_id": "string",
- "user": {
- "external_id": "string",
- "display_name": "string",
- "handle": "string"
}, - "data": "string"
}{- "type": "../dictionary",
- "title": "string",
- "status": 0,
- "detail": "string",
- "instance": "string",
- "class": "transient",
- "scope": "channel",
- "reason": "edit_window_closed"
}Offered when event_delivery contains poll. Returns the events after cursor — all of them when cursor is absent — oldest first, at once when there are any, otherwise as soon as one arrives or after wait seconds with an empty list; Muster waits for the answer up to wait plus 15 seconds. Muster handles the batch and sends next_cursor with its next call, so an event is delivered at least once; the bridge may then forget the events before that cursor. The bridge keeps events that Muster has not passed for at least 24 hours. A cursor the bridge no longer knows is invalid, and Muster polls again without one. Only one Muster replica polls a Connection at a time, but after a handover two calls may overlap; events are deduplicated by id.
| cursor | string <= 1024 characters The |
| wait | integer [ 0 .. 60 ] Default: 25 How long to hold the request open when there are no events, in seconds. |
{- "events": [
- {
- "id": "string",
- "type": "press",
- "occurred_at": "2019-08-24T14:15:22Z",
- "message_id": "string",
- "user": {
- "external_id": "string",
- "display_name": "string",
- "handle": "string"
}, - "data": "string"
}
], - "next_cursor": "string"
}