Przejdź do treści głównej
Developers Webhooks

Post messages with a webhook

A webhook is a URL that posts into your community. Send it JSON from anything that can make an HTTP request, such as CI, monitoring, a cron job or a script, and the message appears in the channel.

What you can do

  • Post text or a cardPlain text, or a card with a title, a colour, markdown, fields and images.
  • Attach filesUp to five files per message: logs, reports, screenshots.
  • Add buttonsLinks, or buttons that change the card or reach your service.
  • Change it laterThe response carries a callback URL to update or delete the message.

In the app

$ curl -X POST "$MSSGS_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Mssgs-Signature: sha256=$SIG" \
-d '{"message_container": {"title": "Build #1847 passed", ...}}'
{"success": true, "message_id": "aZZ1a2b-..."}

One request from CI, one card in #deploys. The name at the top is the name you gave the webhook.

Quick start

  1. Create the webhook

    In the desktop app, open your community's Manage Server → Webhooks, create a webhook, choose the channels it may post in and copy the URL for a channel. It has this shape:

    url
    https://mss.gs/api/v1/webhook/{webhook_guid}/{token}/{channel_guid}
  2. Send a message

    Sign the JSON with the webhook's secret and POST it. A webhook made in the desktop app always has one: copy it from Webhook Secret in the webhook's settings.

    bash
    BODY='{"content": "Build #1847 passed on main"}'
    SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$MSSGS_WEBHOOK_SECRET" | sed 's/^.* //')
    
    curl -X POST "$MSSGS_WEBHOOK_URL" \
      -H "Content-Type: application/json" \
      -H "X-Mssgs-Signature: sha256=$SIG" \
      -d "$BODY"
  3. Read the answer

    The response gives you the message id, and a callback_url to change the message later.

    json
    {
      "success": true,
      "message_id": "aZZ1a2b-...",
      "callback_url": "https://mss.gs/api/v1/trigger-callback/8f14e45f-...",
      "stream_url": "https://mss.gs/api/v1/instant/6b1e...c0?token=a1b2c3d4-..."
    }
Anyone who has the URL and the secret can post into that channel. Keep both out of public repositories and client-side code.

What you can send

A message is either the short form (text with a title and a colour) or a full card, and either can carry buttons and files. The name at the top of the card is always the webhook's own name. In the webhook's settings you also decide whether it may post images and mention people.

Short form

Enough for most alerts.

json
{
  "content": "Server backup completed at 03:00 UTC",
  "color": "green",
  "title": "Backup Bot"
}
FieldTypeWhat it does
contentstringThe message text. Required unless you send a card or files.
colorstringblue (default), green, orange, red, yellow or purple.
titlestringA title above the text. Defaults to the webhook's name.

A full card

Send a message_container for a card with a linked title, a subtitle, markdown, fields and images. Through a webhook a card takes type, color, title, title_url, sub_title, description, fields, avatar_url, image_url, image_base64, images and the loader fields. The status pill, badge, diff stats and folded reasoning are for command replies. Every field is on message cards.

json
{
  "message_container": {
    "type": "embed_message",
    "color": "green",
    "title": "Build #1847 passed",
    "title_url": "https://ci.example.com/builds/1847",
    "description": "All 212 tests green on **main**.",
    "fields": [
      { "field": "Duration", "value": "2m 34s" },
      { "field": "Commit", "value": "1a2b3c4" }
    ]
  }
}
deploys
System
Message from CI

Build #1847 passed

All 212 tests green on main.
Duration
2m 34s
Commit
1a2b3c4

Buttons

Add an actions array to put buttons under the message. How they work is on buttons.

Files

Post real files with a message: a log, a report, a screenshot. They show like any other attachment, as a download row or inline for images, video and audio. A message with only files is fine: leave out content and the card.

json
{
  "message_container": {
    "color": "orange",
    "title": "Log dump: ios",
    "description": "DMs stopped arriving after switching networks"
  },
  "attachments": [
    {
      "name": "mssgs-logs-20260803-141205.log",
      "content_base64": "MjAyNi0wOC0wMyAxNDoxMjowNSBbV1NdIGNvbm5lY3RlZAo=",
      "mime_type": "text/plain"
    }
  ]
}
FieldTypeWhat it does
namestringThe file name it downloads as. Required. A path is reduced to its last part.
content_base64stringThe file's bytes as base64, raw or as a data: URI. mssgs stores the file and keeps only a link on the message.
mime_typestringThe content type of content_base64. Defaults to text/plain.
urlstringA file already hosted on mssgs: a /static/... path or an https://mss.gs/... URL.
Send exactly one of content_base64 or url per file. Both, or neither, is an error.
LimitValue
Files per message5
Size per file, after decoding8 MB
File name200 characters
Whole requestAbout 10 MB. Base64 makes a file a third bigger, so a single file above roughly 7 MB will not fit.

Why url only takes mssgs addresses

A webhook URL often ends up pasted into other dashboards. A leaked one must not let someone make every member's app fetch a file from a server they picked. If your file lives elsewhere, send it as content_base64 and mssgs hosts it.

A failed upload does not fail the message

Files are checked up front but uploaded afterwards. If an upload fails, that file is left out and the rest of the message still posts, without an error: losing the file beats losing the report. If a file matters, check that it arrived.

A file from the command line

bash
BODY='{"attachments":[{"name":"report.csv","content_base64":"'"$(base64 < report.csv | tr -d '\n')"'","mime_type":"text/csv"}]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$MSSGS_WEBHOOK_SECRET" | sed 's/^.* //')

printf '%s' "$BODY" | curl -sS -X POST "$MSSGS_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H "X-Mssgs-Signature: sha256=$SIG" \
  --data-binary @-

Signing requests

A webhook with a secret only accepts requests that prove they know it, and a webhook made in the desktop app always has one (Webhook Secret in its settings). Sign the raw request body with HMAC-SHA256 using the secret, and send the lowercase hex digest in the X-Mssgs-Signature header as sha256=<hex>.

javascript
import crypto from 'node:crypto';

const body = JSON.stringify({ content: 'Deploy finished' });
const signature = crypto.createHmac('sha256', process.env.MSSGS_WEBHOOK_SECRET)
  .update(body)
  .digest('hex');

await fetch(process.env.MSSGS_WEBHOOK_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Mssgs-Signature': `sha256=${signature}`
  },
  body
});
A request without a valid signature gets 401 and {"error": "INVALID_SIGNATURE"}. Only a webhook without a secret, such as one created over MCP without webhook_secret, accepts unsigned requests.

GitHub's own X-Hub-Signature-256 header is accepted too, so a GitHub webhook with the same secret works as is. The signature has no timestamp, so it does not stop a captured request from being sent again: the URL stays the secret that matters.

Responses and errors

A message that was posted comes back with its id and a callback_url to update or delete it for 30 minutes.

json
{
  "success": true,
  "message_id": "aZZ1a2b-...",
  "callback_url": "https://mss.gs/api/v1/trigger-callback/8f14e45f-...",
  "stream_url": "https://mss.gs/api/v1/instant/6b1e...c0?token=a1b2c3d4-..."
}
Read the body as well as the status. A rejected payload carries its reason as an error code in the body. Treat any body with an error key as a failure, whatever the status code.
http
HTTP/1.1 200 OK
Content-Type: application/json

{ "error": "ATTACHMENT_TOO_LARGE" }
javascript
const res = await fetch(webhookUrl, { method: 'POST', headers, body });
const reply = await res.json().catch(() => null);

// A rejected payload still comes back as 200: read the body.
if (!res.ok || (reply && reply.error)) {
  throw new Error(`webhook rejected: ${reply?.error ?? res.status}`);
}

When the message has buttons, the response also has a stream_url: a live stream of the replies, reactions and button presses on that message, open for 10 minutes, or an hour when you send "sse_event_extended_timeout": true. See live updates.

Status codes

StatusWhen
401The webhook has a secret and the signature is missing or wrong.
403This webhook may not post in that channel.
404There is no webhook at this URL.
413The request is too large.
429Too many requests. Slow down and try again.
502The message could not be delivered. Try again.

Error codes

CodeMeaning
MISSING_CONTENTNothing to post: no text, no card and no files.
INVALID_MESSAGE_CONTAINERmessage_container is not an object.
INVALID_MESSAGE_CONTAINER_TYPEThe card type is not embed_message or system_message.
MISSING_MESSAGE_CONTAINER_DESCRIPTIONA card needs a description, unless it is a loader.
MISSING_MESSAGE_CONTAINER_LOADER_TEXTA loader card needs loader_text.
INVALID_WEBHOOK_BINDINGThis webhook may not post in that channel.
INVALID_SIGNATUREThe signature header is missing or wrong.
REQUEST_BODY_TOO_LARGEThe request is over the size limit.
PUBLISH_FAILEDThe message could not be delivered.
INVALID_ACTIONS_FORMAT, INVALID_ACTION_MISSING_FIELDS, DUPLICATE_ACTION_ID, INVALID_TRIGGERS_FORMAT, INVALID_TRIGGER_MISSING_ACTION, INVALID_TRIGGER_ACTION_NOT_ALLOWEDSomething is wrong with a button. See buttons.

File errors

CodeMeaning
INVALID_ATTACHMENTS_FORMATattachments is not a list, or an entry is not an object.
TOO_MANY_ATTACHMENTSMore than five files.
MISSING_ATTACHMENT_NAMEA file has no name.
INVALID_ATTACHMENT_NAMEThe name reduces to nothing usable, such as ...
MISSING_ATTACHMENT_SOURCENeither url nor content_base64.
AMBIGUOUS_ATTACHMENT_SOURCEBoth url and content_base64.
INVALID_ATTACHMENT_BASE64The base64 does not decode.
ATTACHMENT_TOO_LARGEA file is over 8 MB after decoding.
INVALID_ATTACHMENT_URLThe url is not an mssgs address.

Limits

LimitValue
Request sizeAbout 10 MB
Files per message5, of up to 8 MB each
Card descriptionUp to 50,000 bytes. Past 1,000 bytes, members see the start and a Show more button.
Updating the message afterwards30 minutes, through callback_url
Live stream of a message with buttons10 minutes, or an hour on request

Requests are rate-limited. When you get a 429, wait before you send again, and group alerts that arrive in bursts into one message.

GitHub, UniFi and App Store Connect

Point one of these services at a webhook URL and mssgs recognises it and posts a proper card, with no payload to write. See integrations. These answer with {"success": true} and no callback URL.

SourceRecognised byWhat it posts
GitHubThe x-github-event headerPushes, pull requests and reviews, issues and comments, branches and tags, releases. A burst of changes to one issue or pull request is gathered into one card.
UniFi ProtectThe protect-alarm-manager user agentDoorbell rings, motion, and people, vehicles or packages detected by your cameras.
App Store ConnectIts notification body or the x-apple-signature headerApp Store Connect notifications.

Keep building