---
title: "Webhooks: post messages into an mssgs channel"
description: "Send a message or a card into an mssgs channel from CI, monitoring or any script with one HTTP request. Formats, files, signing, errors and limits."
canonical: https://docs.mss.gs/en/webhooks
language: en
---

# 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 card** Plain text, or a card with a title, a colour, markdown, fields and images.

- **Attach files** Up to five files per message: logs, reports, screenshots.

- **Add buttons** Links, or buttons that change the card or reach your service.

- **Change it later** The response carries a callback URL to update or delete the message.

In the app

#### Build #1847 passed

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

- [Quick start](#quick-start)

- [What you can send](#format)

- [Files](#attachments)

- [Signing](#signing)

- [Responses and errors](#responses)

- [Limits](#limits)

- [GitHub, UniFi, App Store](#special)

## Quick start

### 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}
```

### 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"
```

### 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-..."
}
```

## 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"
}
```

| Field | Type | What it does |
| --- | --- | --- |
| content | string | The message text. Required unless you send a card or files. |
| color | string | blue (default), green , orange , red , yellow or purple . |
| title | string | A 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](https://docs.mss.gs/en/bots).

```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" }
    ]
  }
}
```

#### Build #1847 passed

### Buttons

Add an actions array to put buttons under the message. How they work is on [buttons](https://docs.mss.gs/en/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"
    }
  ]
}
```

| Field | Type | What it does |
| --- | --- | --- |
| name | string | The file name it downloads as. Required. A path is reduced to its last part. |
| content_base64 | string | The file's bytes as base64, raw or as a data: URI. mssgs stores the file and keeps only a link on the message. |
| mime_type | string | The content type of content_base64 . Defaults to text/plain . |
| url | string | A file already hosted on mssgs: a /static/... path or an https://mss.gs/... URL. |

| Limit | Value |
| --- | --- |
| Files per message | **5** |
| Size per file, after decoding | **8 MB** |
| File name | 200 characters |
| Whole request | About 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
});
```

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-..."
}
```

```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](https://docs.mss.gs/en/live-updates).

### Status codes

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

### Error codes

| Code | Meaning |
| --- | --- |
| MISSING_CONTENT | Nothing to post: no text, no card and no files. |
| INVALID_MESSAGE_CONTAINER | message_container is not an object. |
| INVALID_MESSAGE_CONTAINER_TYPE | The card type is not embed_message or system_message . |
| MISSING_MESSAGE_CONTAINER_DESCRIPTION | A card needs a description, unless it is a loader. |
| MISSING_MESSAGE_CONTAINER_LOADER_TEXT | A loader card needs loader_text . |
| INVALID_WEBHOOK_BINDING | This webhook may not post in that channel. |
| INVALID_SIGNATURE | The signature header is missing or wrong. |
| REQUEST_BODY_TOO_LARGE | The request is over the size limit. |
| PUBLISH_FAILED | The 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_ALLOWED | Something is wrong with a button. See [buttons](https://docs.mss.gs/en/buttons). |

### File errors

| Code | Meaning |
| --- | --- |
| INVALID_ATTACHMENTS_FORMAT | attachments is not a list, or an entry is not an object. |
| TOO_MANY_ATTACHMENTS | More than five files. |
| MISSING_ATTACHMENT_NAME | A file has no name. |
| INVALID_ATTACHMENT_NAME | The name reduces to nothing usable, such as .. . |
| MISSING_ATTACHMENT_SOURCE | Neither url nor content_base64 . |
| AMBIGUOUS_ATTACHMENT_SOURCE | Both url and content_base64 . |
| INVALID_ATTACHMENT_BASE64 | The base64 does not decode. |
| ATTACHMENT_TOO_LARGE | A file is over 8 MB after decoding. |
| INVALID_ATTACHMENT_URL | The url is not an mssgs address. |

## Limits

| Limit | Value |
| --- | --- |
| Request size | About 10 MB |
| Files per message | 5, of up to 8 MB each |
| Card description | Up to 50,000 bytes. Past 1,000 bytes, members see the start and a **Show more** button. |
| Updating the message afterwards | 30 minutes, through callback_url |
| Live stream of a message with buttons | 10 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](https://mss.gs/en/integrations). These answer with {"success": true} and no callback URL.

| Source | Recognised by | What it posts |
| --- | --- | --- |
| GitHub | The x-github-event header | Pushes, 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 Protect | The protect-alarm-manager user agent | Doorbell rings, motion, and people, vehicles or packages detected by your cameras. |
| App Store Connect | Its notification body or the x-apple-signature header | App Store Connect notifications. |

## Keep building
