Webhooks & Integrations
Build bots, automations and integrations with mssgs webhooks and triggers
Bot Messages
Rich embed cards, interactive buttons and live-updating messages for bots.
Release notes
New features and fixes in every mssgs release.
Getting Started
With mssgs webhooks you can send messages to channels and build interactive bots that respond to commands.
Create a webhook
Go to Server Settings > Integrations and create a new webhook or trigger.
Copy the URL
Copy the webhook URL or trigger GUID for your integration.
Start building
Send JSON payloads to the webhook URL to post messages.
Two ways to integrate
Incoming Webhooks: Send messages to mssgs from external tools (CI/CD, monitoring, etc.)
Webhook Triggers: Build interactive bots that respond to commands and button clicks.
Incoming Webhooks
Send messages to a channel via a webhook URL. Perfect for CI/CD notifications, monitoring alerts, and other automations.
/api/v1/webhook/:server_guid/:channel_guid
Request structure
Send a POST request with your message payload as JSON body to the webhook URL. Generate the URL in the app under Manage Server → Webhooks; the URL itself authenticates the request, so you don't send a token in the body.
If the webhook has a Webhook Secret set, also send a signature of the raw request body in the X-Mssgs-Signature header, in the form sha256=<hex>. An unsigned request then gets 401 with {"error": "INVALID_SIGNATURE"}.
{
"content": "Hello from my integration!",
"color": "green",
"title": "My Bot"
}
Simple Format
The simplest way to send a message.
{
"content": "Server backup completed at 03:00 UTC",
"color": "green",
"title": "Backup Bot"
}
| Field | Type | Description |
|---|---|---|
content |
string | Message text required |
color |
string | blue, green, orange, red, yellow, purple |
title |
string | Title above the message |
title_url |
string | Makes the title a clickable link |
sub_title |
string | Smaller text below the title |
avatar_url |
string | Avatar image URL |
Full Format (message_container)
For more control over the message, use the full message_container object.
{
"message_container": {
"type": "embed_message",
"color": "blue",
"title": "Order Update",
"description": "Your order #12345 has been shipped!",
"title_url": "https://example.com/orders/12345",
"sub_title": "Estimated delivery: Tomorrow",
"bot_name": "Order Bot",
"fields": [
{ "field": "Status", "value": "Shipped" },
{ "field": "Tracking", "value": "ABC123456" }
]
},
"actions": [...]
}
| Field | Type | Description |
|---|---|---|
type |
string | embed_message (default) or system_message |
color |
string | Accent color of the message |
title |
string | Title of the message |
description |
string | Main text required |
title_url |
string | Link behind the title |
sub_title |
string | Subtitle text |
bot_name |
string | Custom bot name |
avatar_url |
string | Avatar image |
fields |
array | Key-value fields |
Fields
Add structured key-value data to your message.
{
"fields": [
{ "field": "Status", "value": "Completed" },
{ "field": "Duration", "value": "2m 34s" },
{ "field": "Environment", "value": "Production" }
]
}
Attachments
Post real files with a webhook message: a log dump, a report, a capture. They render like the attachments on any other message, as downloadable file rows or as inline media for images, video and audio.
An attachment-only message is valid. Send attachments without content and without message_container, and the card is just the file.
{
"message_container": {
"type": "embed_message",
"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"
}
]
}
Attachment fields
| Field | Type | Description |
|---|---|---|
name |
string | File name, and the name it downloads as. Reduced to its base name; a path or a ../ prefix is stripped, not honoured required |
content_base64 |
string | The file's bytes as base64 (raw, or a data: URI). Uploaded into the channel server-side and delivered as a hosted file; the base64 itself is never stored on the message |
mime_type |
string | Content type of content_base64. Wins over the type inside a data: URI. Defaults to text/plain |
url |
string | An mssgs-hosted file you already uploaded: a path (/static/...) or an absolute https://mss.gs/... URL, which is normalized down to its path |
Send exactly one of content_base64 or url per entry. Sending both is an error, sending neither is an error.
Limits
| Limit | Value |
|---|---|
| Attachments per message | 5 |
| Size per attachment (decoded) | 8 MB |
| File name length | 200 characters |
The 8 MB cap is per file after base64 decoding. Base64 expands by 4/3, so a full-size attachment is about 10.7 MB of JSON. The request cap is 16 MB, so one maximal attachment fits and two do not.
Why url only accepts mssgs hosts
A webhook URL is a bearer credential that often gets pasted into third-party dashboards. A leaked one must not become a way to render arbitrary remote content, or to make every client in the channel issue a request to a host the sender picked. If your bytes live on another host, send them as content_base64 and mssgs hosts them for you.
Accepted: /static/..., https://mss.gs/..., https://www.mss.gs/.... Anything else returns INVALID_ATTACHMENT_URL.
Check the response body, not the HTTP status
The public webhook route answers 200 OK even when your payload was rejected; the error only exists in the body. Treat a JSON body containing error as a failure regardless of status code. See Errors for the codes and the statuses that are returned honestly.
A failed upload is silent
Validation happens up front, but the upload itself happens later. If that upload fails, that attachment is dropped and the rest of the message still posts, with no error returned. This is intentional: losing the file beats losing the report. If a file is critical, verify it landed instead of assuming a non-error response means it did. If every attachment fails to upload, the attachments key is removed from the message entirely.
Attachment only
{
"attachments": [
{ "name": "nightly-report.csv", "url": "/static/mu/ab12/nightly-report.csv" }
]
}
curl -sS -X POST "$MSSGS_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"attachments":[{"name":"report.csv","content_base64":"'"$(base64 < report.csv | tr -d '\n')"'","mime_type":"text/csv"}]}'
async function postWebhookAttachment (webhookUrl, name, bytes, mimeType) {
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
name,
content_base64: Buffer.from(bytes).toString('base64'),
mime_type: mimeType
}]
})
});
// The public webhook route answers 200 even when the payload was rejected;
// the error only exists in the body. Check both.
const body = await res.json().catch(() => null);
if (!res.ok || (body && body.error)) {
throw new Error(`webhook rejected: ${body?.error ?? res.status}`);
}
return body;
}
Good to know
Attachments count as content. A message with attachments and nothing else passes the content check, so you don't need filler text.
allow_images: false does not block attachments. That per-webhook option strips pictures out of the card itself (image_url, image_base64, the images gallery). It does not touch attachments, so a webhook with images disabled can still post a .png as a file attachment, which clients render inline like any other image attachment. There is currently no per-webhook switch for attachments; the only control is the webhook URL itself.
A hosted attachment is stored as {name, url, guid} on the message. The content_base64 you sent is never persisted.
Build your embed
Edit the fields or the JSON payload. Both stay in sync. Watch the message render exactly as it will in a channel. This is the real webhook body; copy it when it looks right.
Webhook Triggers
Webhook triggers let you connect external services to your server. When a user types a matching command (e.g. /help) or clicks an action button, the backend POSTs to your webhook URL. Your webhook responds with JSON to post a message back to the channel.
User types a command
A user sends a message starting with your trigger's prefix, e.g. /help
mssgs calls your webhook
The backend sends a POST request to your configured webhook URL with the message data.
You respond with JSON
Your server returns a JSON response with the message content, embeds and actions.
Message appears in channel
mssgs displays the response as a message in the channel.
Request Your Webhook Receives
When a user sends a message that starts with your trigger's trigger_match prefix:
{your_webhook_url}
{
"server_guid": "abc12345-...",
"channel_guid": "def67890-...",
"trigger_match": "/help",
"message": {
"id": "d01ZZdef6-xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx",
"content": "/help how do I create a channel?",
"member_guid": "member-guid-here",
"user_guid": "user-guid-here",
"cms": 1234567890123,
"group_guids": ["group-guid-1", "group-guid-2"]
},
"callback_url": "https://mss.gs/api/v1/trigger-callback/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"stream_url": "https://mss.gs/api/v1/instant/6b1e...c0?token=a1b2c3d4-..."
}
Request via Action Button
When triggered via a trigger:{guid} action button, the content is always "[Action Triggered]" and includes the action's payload:
{
"server_guid": "abc12345-...",
"channel_guid": "def67890-...",
"trigger_match": "/help",
"message": {
"id": "d01ZZdef6-xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx",
"content": "[Action Triggered]",
"member_guid": "member-guid-here",
"user_guid": "user-guid-here",
"cms": 1234567890123,
"group_guids": ["group-guid-1", "group-guid-2"],
"is_action_button": true,
"action_payload": {
"custom_key": "custom_value"
}
},
"callback_url": "https://mss.gs/api/v1/trigger-callback/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"stream_url": "https://mss.gs/api/v1/instant/6b1e...c0?token=a1b2c3d4-..."
}
Request Fields
| Field | Type | Description |
|---|---|---|
server_guid |
string | The server where the trigger was fired |
channel_guid |
string | The channel where the message was sent |
trigger_match |
string | The prefix that matched (e.g. /help) |
message |
object | The message that triggered the webhook. For action-button triggers this is a synthetic "[Action Triggered]" message, not the message the button was on |
message.id |
string | Unique message ID. For action-button triggers it is freshly generated for the synthetic message — it is not the ID of the message the button was on (that ID never reaches the webhook body) |
message.content |
string | Full message text (command match) or "[Action Triggered]" (action button) |
message.member_guid |
string | The server member who triggered it |
message.user_guid |
string | The user's global GUID |
message.group_guids |
array | The server group GUIDs the member belongs to (use to check roles like admin) |
message.cms |
number | Timestamp in milliseconds |
message.is_action_button |
boolean | true when triggered via an action button (not present for command matches) |
message.action_payload |
object | Custom data from the action button (only present for action triggers) |
callback_url |
string | Unique URL to update or delete the response message (valid for 30 minutes) |
stream_url |
string | Unique instant SSE stream for live interactions (replies, reactions, button presses) with your response message. Open it to listen; valid for 10 minutes |
Response Format
Your webhook must respond with 200 status and a JSON body. The response becomes a message in the channel.
Simple response
{
"content": "Hello! How can I help you?"
}
Embed response
{
"message_container": {
"type": "embed_message",
"color": "blue",
"title": "Help",
"description": "Here's how to create a channel...",
"title_url": "https://docs.example.com/channels",
"sub_title": "Channel Guide",
"avatar_url": "https://example.com/bot-avatar.png"
}
}
Full response with actions
{
"content": "Here's what I found:",
"message_container": {
"type": "embed_message",
"color": "green",
"title": "Search Results",
"description": "Found 3 matching items.",
"title_url": "https://example.com/results",
"sub_title": "Query: channels",
"avatar_url": "https://example.com/bot-avatar.png"
},
"actions": [
{
"type": "button",
"text": "View Details",
"color": "blue",
"triggers": [
{
"action": "ws:send",
"payload": {
"method": "USER_REQUESTED_TRIGGER",
"server_guid": "server-guid",
"channel_guid": "channel-guid",
"trigger_guid": "details-trigger-guid",
"action": { "result_id": "42" }
}
}
]
},
{
"text": "Open Docs",
"type": "url:https://docs.example.com"
}
]
}
Response Fields
| Field | Type | Description |
|---|---|---|
content |
string | Plain text message content |
message_container |
object | Rich embed/system message display |
message_container.type |
string | embed_message (default) or system_message |
message_container.color |
string | blue, green, orange, red (default: blue) |
message_container.title |
string | Embed title |
message_container.description |
string | Embed body text |
message_container.title_url |
string | Makes the title a clickable link |
message_container.sub_title |
string | Smaller text below the title |
message_container.avatar_url |
string | Avatar image URL shown alongside the embed |
actions |
array | Action buttons (see Actions) |
At least one of content, message_container, or actions must be present; otherwise no message is created.
The message author is your trigger's post_app_name (configured in server settings). If not set, the server name is used.
Timeout
Your webhook must respond within 5 seconds. If it times out, an error message is shown to the user. Use the callback URL if you need more time to process.
Actions
Actions are interactive buttons displayed below your message. There are three kinds: url:{url} (open a link), trigger:{guid} (fire a real webhook trigger), and webhook_action (server-driven effects; recommended for interactive buttons).
Action Fields
| Field | Type | Description |
|---|---|---|
type |
string | "webhook_action" (server-driven), or shorthand "trigger:{guid}" / "url:{url}" required |
text |
string | Button display text (also accepts label) required |
color |
string | green, blue, purple (primary) or red, orange, yellow (secondary) |
id |
string | For webhook_action: stable button id, sent back on press so the backend knows which button fired |
payload |
object | Data passed back on press (trigger:{guid} and webhook_action) |
triggers |
array | Server-side effect steps run when a webhook_action button is pressed |
Simple Shorthands
For basic actions, use the type shorthand:
| Type | Description |
|---|---|
trigger:{guid} |
Fires another webhook trigger by its GUID. The trigger receives message.action_payload with the action's payload. |
url:{url} |
Opens the URL in the user's browser |
{
"actions": [
{
"text": "Check Status",
"type": "trigger:abc123-trigger-guid",
"payload": { "order_id": "12345" }
},
{
"text": "View Order",
"type": "url:https://example.com/orders/12345"
}
]
}
What happens when an action is clicked?
When the user clicks "Check Status", the target trigger's webhook receives a new request with message.content set to "[Action Triggered]", message.is_action_button set to true, and message.action_payload set to { "order_id": "12345" }.
Interactive Buttons (server-driven)
A webhook_action button is handled by the backend; no real trigger required. On press, the client sends a WEBHOOK_MESSAGE_ACTION command carrying the message id, the button's id, and its payload. The backend then relays the press to the message's instant SSE stream (if a device is listening), runs the button's server-side triggers steps, and broadcasts the result to everyone in the channel. Because those steps are read from the stored message (never from the client), a member can only run what you defined.
Server-side trigger steps
A button's triggers array lists the effect steps that run when it's pressed, in order. They execute on the backend and broadcast to everyone in the channel:
| action | Effect (broadcast to everyone) | Fields |
|---|---|---|
update_message |
Replace the container / buttons; edit text, recolor or swap buttons, remove buttons (actions: []), or swap the image |
message_container, actions, content |
remove_message |
Delete the message | None |
add_reaction |
Add a reaction, as the pressing member | emoji |
add_reply |
Post a reply, as the pressing member | content |
Steps run in order and are best-effort; a failing step is logged and the rest still run. To fire a real trigger instead, use the trigger:{guid} shorthand, not triggers.
Example: self-updating "Open Gate" button
Everyone gets instant broadcast feedback from the server-side steps; the edge device sets the authoritative final state via the callback URL once the gate physically opens.
{
"message_container": { "color": "yellow", "title": "Front Door", "description": "Doorbell rang" },
"actions": [
{
"type": "webhook_action",
"id": "open_gate",
"text": "Open Gate",
"color": "green",
"payload": { "btn": "open_gate" },
"triggers": [
{
"action": "update_message",
"message_container": { "color": "blue", "title": "Front Door", "description": "Opening..." },
"actions": [ { "type": "webhook_action", "id": "opening", "text": "Opening...", "color": "blue" } ]
},
{ "action": "add_reaction", "emoji": ":white_check_mark:" }
]
}
]
}
What happens when "Open Gate" is pressed
1. Everyone sees the button become "Opening..." and a checkmark reaction appear; the server-side steps, broadcast by the backend (not just the presser).
2. Your edge device listening on the instant stream receives the press, opens the gate, then PUTs the callback_url to set the authoritative final state ("Opened").
Removed: client-only steps
The old client-only local:update_message / local:remove_message steps are gone. Use the server-side update_message / remove_message steps instead; they broadcast to everyone, not just the presser. A press on a message that no longer exists returns MESSAGE_NOT_FOUND.
Rate limit
Users are rate-limited to prevent spam when firing triggers.
Callback URL
Every webhook request includes a callback_url: a unique, token-authenticated URL that your service can call to update or delete the response message after it was posted. The URL is valid for 30 minutes.
Use cases
- Updating a "processing..." message with final results
- Showing live progress (e.g. build status, deployment)
- Removing a message after it's no longer relevant
Update Message
{callback_url}
{
"content": "Updated text content",
"message_container": {
"color": "green",
"title": "Build Complete",
"description": "All 42 tests passed."
},
"actions": []
}
All fields are optional; only include the ones you want to change. To remove action buttons, send "actions": [].
Delete Message
{callback_url}
No request body needed. The message is permanently deleted from the channel.
Responses
// 200 OK
{ "success": true }
// 404 Not Found (callback token expired or invalid)
{ "error": "TOKEN_NOT_FOUND" }
// 400 Bad Request (no update fields provided)
{ "error": "MISSING_FIELDS" }
Example: Progress Updates
// 1. Your webhook responds immediately with a "loading" message
// Response:
{
"message_container": {
"color": "blue",
"title": "Deploying...",
"description": "Starting deployment to production."
}
}
// 2. Your service updates the message as progress continues
// PUT {callback_url}
{
"message_container": {
"color": "blue",
"title": "Deploying...",
"description": "Step 2/3: Running migrations."
}
}
// 3. Final update when done
// PUT {callback_url}
{
"message_container": {
"color": "green",
"title": "Deploy Complete",
"description": "v2.1.0 is now live on production."
},
"actions": [
{
"label": "View Logs",
"type": "url:https://example.com/deploys/123/logs"
}
]
}
Callback URL lifetime
Valid for 30 minutes from when the trigger fires. After that, update/delete requests return 404. Once you delete the message, the callback URL is consumed and cannot be used again.
See it happen
The full trigger loop, live: command, JSON reply, button tap, callback update.
mssgs → POST your-webhook
A command fires your webhook
Someone types /deploy. mssgs POSTs the message to your endpoint, including a callback_url for this message.
200 ← message_container + actions
Your reply becomes a card
Respond with JSON and the card appears in the channel: title, description, status and buttons.
mssgs → POST your-webhook · [Action Triggered]
A button tap comes back to you
The tap fires the trigger to your server again, with a fresh callback_url.
PUT {callback_url} → 200
You update the card in place
First a loader while you work, then the final state. Everyone in the channel sees it update live.
MCP: AI Assistants
The mssgs desktop app ships a built-in MCP server (Model Context Protocol), so AI assistants like Claude can read and act in your workspace. It runs locally on your machine and talks to your logged-in mssgs session; no separate bot account or hosting needed.
http://127.0.0.1:7444/mcp
Authenticated with a Bearer token from the app's AI / MCP settings page: enable the server, pick the channels to expose and press "Copy config"; the copied block already contains your port and token. The server listens on localhost only (default port 7444) and runs while mssgs is open and you are signed in.
claude mcp add --transport http mssgs http://127.0.0.1:7444/mcp \
--header "Authorization: Bearer <your-token>"
Real-time events over SSE
Open the MCP endpoint as a stream (GET /mcp with Accept: text/event-stream) and you receive JSON-RPC notifications as they happen:
| Notification | Fires on |
|---|---|
notifications/mssgs/message | New messages in channels you can see |
notifications/mssgs/bot_event | Button presses, reactions and replies on messages your assistant posted via send_bot_message |
notifications/mssgs/presence | Presence changes |
notifications/mssgs/typing | Typing indicators |
notifications/mssgs/call | Voice call events |
Interactive bot cards from your assistant
Post an embed card with buttons via send_bot_message, listen for bot_event on the stream (or poll get_bot_events), and update the card in place with edit_message. Same card model as the bot messages docs.
Bot tools
| Tool | Description |
|---|---|
send_bot_message | Post a message or embed card (with buttons) as your assistant |
get_bot_events | Fetch queued button presses, reactions and replies on your bot messages |
edit_message | Update a posted message's message_container in place |
set_bot_status | Set the assistant's presence/status |
Bot tools are available when you are signed in as a bot account. The full tool list (channels, search, files, calls and more) is documented in the app on the AI / MCP settings page.
Errors
When errors occur, you receive a JSON error response.
{
"error": "ERROR_CODE"
}
Error codes
| Code | Description |
|---|---|
INVALID_TOKEN |
Invalid authentication token |
MISSING_REQUIRED_FIELDS |
Required fields are missing in data |
MISSING_CONTENT |
Message requires content or message_container.description |
TOKEN_NOT_FOUND |
Callback token expired or invalid |
MISSING_FIELDS |
No update fields provided in callback request |
UNSUPPORTED_GITHUB_EVENT |
GitHub event type not supported |
UNSUPPORTED_UNIFI_PROTECT_EVENT |
UniFi Protect event not supported |
INVALID_SIGNATURE |
The webhook has a secret set and the X-Mssgs-Signature header is missing or wrong |
Attachment error codes
| Code | Description |
|---|---|
INVALID_ATTACHMENTS_FORMAT |
attachments is not an array, or an entry is not an object |
TOO_MANY_ATTACHMENTS |
More than 5 attachments |
MISSING_ATTACHMENT_NAME |
name is absent or blank |
INVALID_ATTACHMENT_NAME |
name reduces to nothing usable (., .., /) |
MISSING_ATTACHMENT_SOURCE |
Neither url nor content_base64 |
AMBIGUOUS_ATTACHMENT_SOURCE |
Both url and content_base64 |
INVALID_ATTACHMENT_BASE64 |
content_base64 does not decode |
ATTACHMENT_TOO_LARGE |
Decoded size is over 8 MB |
INVALID_ATTACHMENT_URL |
url is not an mssgs-hosted path |
Status codes on incoming webhooks
Your client must check the body, not the HTTP status
The public webhook URL returns 200 OK for a rejected payload; the error code only exists in the response body. Checking response.ok alone reports every rejection as a success. Treat a JSON body containing error as a failure regardless of status code.
HTTP/1.1 200 OK
Content-Type: application/json
{ "error": "ATTACHMENT_TOO_LARGE" }
These statuses are returned honestly, because they are decided before your payload is processed:
| Status | When |
|---|---|
401 |
The webhook has a secret and the signature is missing or wrong (INVALID_SIGNATURE) |
403 |
Webhook token or channel binding mismatch |
404 |
Webhook not found |
200 |
Everything else, including rejected payloads. Read the body |
Success response
{
"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-..."
}
Use callback_url to update or delete the message later. When your message includes action buttons, the response also contains a stream_url; an instant SSE stream for live interactions (replies, reactions, button presses) with that message.
Examples
Simple notification
curl -X POST https://mss.gs/api/v1/webhook/SERVER_GUID/CHANNEL_GUID \
-H "Content-Type: application/json" \
-d '{
"content": "Build completed successfully!"
}'
Colored alert with link
{
"content": "Build #123 completed!",
"color": "green",
"title": "CI/CD Pipeline",
"title_url": "https://github.com/org/repo/actions/runs/123"
}
Error alert
{
"message_container": {
"type": "system_message",
"color": "red",
"title": "Alert: Database Error",
"description": "Connection failed: timeout after 30s",
"sub_title": "prod-db-01"
}
}
Deployment approval with actions
{
"message_container": {
"color": "yellow",
"title": "Deployment Request",
"description": "User @johndoe requested a deployment to production."
},
"actions": [
{
"label": "Approve",
"type": "trigger:approve-deploy-trigger-guid",
"payload": { "deploy_id": "dep_123", "env": "production" }
},
{
"label": "Reject",
"type": "trigger:reject-deploy-trigger-guid",
"payload": { "deploy_id": "dep_123" }
},
{
"label": "View Changes",
"type": "url:https://github.com/org/repo/compare/main...deploy"
}
]
}
Trigger response: private message
{
"message_container": {
"description": "This is a private response only you can see."
},
"visible_to_member_guids": ["<member_guid from request>"],
"ephemeral": true
}
Trigger response: interactive menu
{
"message_container": {
"title": "What would you like to do?",
"description": "Choose an option below:"
},
"actions": [
{
"label": "Get Help",
"type": "trigger:help-trigger-guid"
},
{
"label": "View Stats",
"type": "trigger:stats-trigger-guid",
"payload": { "period": "weekly" }
}
]
}
Special Webhooks
mssgs automatically recognizes certain webhook types.
GitHub Webhooks
Automatically detected via the x-github-event header. Supported events: Push, Pull Requests, Issues, Releases, and more.
UniFi Protect Webhooks
Automatically detected via user-agent: protect-alarm-manager header.
Notes
- Timeout: Your webhook must respond within 5 seconds. If it times out, an error message is shown to the user. Use the callback URL if you need more time to process.
- Message persistence: Trigger response messages are saved to the database and appear in channel history.
- Callback URL lifetime: Valid for 30 minutes from when the trigger fires. After that, update/delete requests return
404. - Rate limiting: Users are rate-limited to prevent spam when firing triggers. Incoming webhooks are limited to 300 requests per 60 seconds per webhook URL.
Questions?
We're happy to help you with your integration.
developers@mss.gs