Muster Bridge protocol (1.0.0)

Download OpenAPI specification:

License: MIT

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.

Info

What the bridge is and what it offers.

Describe the bridge

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.

Authorizations:
bridgeToken

Responses

Response samples

Content type
application/json
{
  • "protocol_version": "1",
  • "bridge": {
    },
  • "messenger": {
    },
  • "capabilities": {
    },
  • "limits": {
    },
  • "user_lookup": "email",
  • "event_delivery": [
    ],
  • "link_url_template": "string"
}

Messages

Root messages and other messages in a channel, and their edits.

Post a message to a channel

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.

Authorizations:
bridgeToken
header Parameters
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.

Request Body schema: application/json
required
status
required
string (Status)
Enum: "firing" "acknowledged" "snoozed" "resolved" "storm"

The status of the Alert Group the message shows; storm for a Storm summary.

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

#N and the Alert Group's title, plain text; shown as the heading and linked to link.

required
object (Link)
body
required
string

CommonMark, as the description of this protocol says; at most limits.max_body_length code points.

required
Array of objects (Button)

In order. Empty when the message has none, always empty without capabilities.buttons.

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 (true) or Quiet (false).

Responses

Request samples

Content type
application/json
{
  • "channel_id": "ch-alerts",
  • "kind": "root",
  • "status": "firing",
  • "color": "#d32f2f",
  • "emoji": "🔴",
  • "title": "#42 HighErrorRate",
  • "link": {},
  • "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": [
    ],
  • "mentions": [
    ],
  • "notify": true
}

Response samples

Content type
application/json
{
  • "message_id": "string",
  • "thread_ref": "string",
  • "truncated": true,
  • "replayed": true
}

Replace a message with a new state

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.

Authorizations:
bridgeToken
path Parameters
message_id
required
string [ 1 .. 512 ] characters

The message_id the bridge returned when the message was posted; opaque to Muster.

Request Body schema: application/json
required
status
required
string (Status)
Enum: "firing" "acknowledged" "snoozed" "resolved" "storm"

The status of the Alert Group the message shows; storm for a Storm summary.

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

#N and the Alert Group's title, plain text; shown as the heading and linked to link.

required
object (Link)
body
required
string

CommonMark, as the description of this protocol says; at most limits.max_body_length code points.

required
Array of objects (Button)

In order. Empty when the message has none, always empty without capabilities.buttons.

Responses

Request samples

Content type
application/json
{
  • "status": "firing",
  • "color": "string",
  • "emoji": "string",
  • "title": "string",
  • "link": {},
  • "body": "string",
  • "buttons": [
    ]
}

Response samples

Content type
application/json
{
  • "truncated": true
}

Delete a message of the bot (optional)

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.

Authorizations:
bridgeToken
path Parameters
message_id
required
string [ 1 .. 512 ] characters

The message_id the bridge returned when the message was posted; opaque to Muster.

Responses

Response samples

Content type
application/problem+json
{
  • "title": "Unauthorized",
  • "status": 401,
  • "class": "forbidden"
}

Threads

Replies in the Thread of a message.

Reply in the Thread of a message

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.

Authorizations:
bridgeToken
path Parameters
message_id
required
string [ 1 .. 512 ] characters

The message_id the bridge returned when the message was posted; opaque to Muster.

header Parameters
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.

Request Body schema: application/json
required
thread_ref
required
string or null <= 512 characters

The last thread_ref Muster received for the message — from postMessage or an earlier reply — or null when it has none.

body
required
string

CommonMark, at most limits.max_body_length code points.

required
Array of objects (Button)
required
Array of objects (Mention)
notify
required
boolean

Loud (true) or Quiet (false).

Responses

Request samples

Content type
application/json
{
  • "thread_ref": "string",
  • "body": "string",
  • "buttons": [
    ],
  • "mentions": [
    ],
  • "notify": true
}

Response samples

Content type
application/json
{
  • "message_id": "string",
  • "thread_ref": "string",
  • "truncated": true,
  • "replayed": true
}

Channels

Checking a channel without posting to it.

Check that the bot can post and edit in a channel

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.

Authorizations:
bridgeToken
path Parameters
channel_id
required
string [ 1 .. 512 ] characters

The channel as the messenger names it and as the Admin entered it on the Bridge Destination.

Responses

Response samples

Content type
application/json
{
  • "channel_id": "string",
  • "name": "string"
}

People

Looking people up and messaging them directly, for Account links.

Find a person by handle (optional)

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.

Authorizations:
bridgeToken
query Parameters
handle
required
string [ 1 .. 320 ] characters

The handle as the person entered it, in the kind user_lookup declares.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Send a direct message from the bot (optional)

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.

Authorizations:
bridgeToken
header Parameters
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.

Request Body schema: application/json
required
user_external_id
required
string [ 1 .. 512 ] characters
body
required
string [ 1 .. 2000 ] characters

CommonMark.

Responses

Request samples

Content type
application/json
{
  • "user_external_id": "string",
  • "body": "string"
}

Response samples

Content type
application/json
{
  • "message_id": "string",
  • "replayed": true
}

Answers

Answers to the person behind an event.

Answer the person behind an event

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.

Authorizations:
bridgeToken
header Parameters
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.

Request Body schema: application/json
required
event_id
required
string^[!-~]{1,200}$
text
required
string [ 1 .. 500 ] characters

Plain text; a pop-up may shorten it.

Responses

Request samples

Content type
application/json
{
  • "event_id": "string",
  • "text": "string"
}

Response samples

Content type
application/json
{
  • "shown_as": "ephemeral",
  • "replayed": true
}

Events

Presses, Thread messages, Account link requests and deleted messages, polled by Muster or pushed to it.

Push an event to Muster Webhook

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.

header Parameters
webhook-id
required
string^[!-~]{1,200}$

The event's id, kept across retries.

webhook-timestamp
required
string^[0-9]{1,12}$

Unix time of this attempt, in seconds.

webhook-signature
required
string >= 4 characters

v1,<base64 HMAC-SHA256>; several signatures separated by spaces are allowed while a secret is rotated.

Request Body schema: application/json
required
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 webhook-id header.

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 data, unchanged.

Responses

Request samples

Content type
application/json
Example
{
  • "id": "string",
  • "type": "press",
  • "occurred_at": "2019-08-24T14:15:22Z",
  • "message_id": "string",
  • "user": {
    },
  • "data": "string"
}

Response samples

Content type
application/problem+json
{
  • "type": "../dictionary",
  • "title": "string",
  • "status": 0,
  • "detail": "string",
  • "instance": "string",
  • "class": "transient",
  • "scope": "channel",
  • "reason": "edit_window_closed"
}

Long-poll for events

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.

Authorizations:
bridgeToken
query Parameters
cursor
string <= 1024 characters

The next_cursor of the previous answer; opaque to Muster.

wait
integer [ 0 .. 60 ]
Default: 25

How long to hold the request open when there are no events, in seconds.

Responses

Response samples

Content type
application/json
{
  • "events": [
    ],
  • "next_cursor": "string"
}