Update messages live
A message does not have to stay the way it was posted. Show progress while a job runs, swap a loader for the result, take the buttons away once someone decided, or delete it. Everyone in the channel sees the change at once.
What you can do
- Update the cardChange the text, the colour and the buttons, in place.
- Show progressA loader that moves through steps, then the result.
- Delete itRemove a message once it is no longer true.
- Listen to itReplies, reactions and button presses on your message, live.
In the app
Posted with a loader
Updated: step 2 of 3
Updated: done, with a button
One message, updated twice through its callback URL. Nobody sees three messages, only one that changes.
Quick start
Keep the callback URL
Every webhook post and every command request comes with a
callback_urlfor that message.bashBODY='{"message_container": {"color": "blue", "loader": true, "loader_text": "Deploying…"}}' 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" # {"success": true, "message_id": "...", "callback_url": "https://mss.gs/api/v1/trigger-callback/8f14e45f-..."}PUT to update
Send the new state. The card changes in place for everyone.
bashcurl -X PUT "$CALLBACK_URL" -H "Content-Type: application/json" \ -d '{"message_container": {"color": "green", "title": "Deploy complete", "description": "v2.1 is live."}}'DELETE to remove
No body needed.
bashcurl -X DELETE "$CALLBACK_URL"
Update a message
PUT JSON to the callback_url. Send only what you want to change.
| Field | Type | What it does |
|---|---|---|
message_container | object | The new card. See message cards. |
actions | array | New buttons. "actions": [] takes them all away; leaving the field out keeps them. |
content | string | New text. |
title, description, color, loader, ... | string | Shorthand: card fields at the top level are wrapped into a card for you. |
message_container replaces the old card as a whole: a field you leave out is gone. Only its type, name and avatar carry over. So send the complete card every time.Responses
| Status | Code | Meaning |
|---|---|---|
200 | {"success": true} | Accepted. The update follows right after. |
400 | MISSING_FIELDS | Nothing to update in the body. |
400 | INVALID_BODY | The body is not valid JSON. |
400 | a button code | Something is wrong with a button, see buttons. |
401 | INVALID_TOKEN | The URL is not valid. |
404 | TOKEN_NOT_FOUND | The URL expired or was used to delete the message. |
502 | PUBLISH_FAILED | The update could not be delivered. Try again. |
How long it works
30 minutes from the moment the message was posted, or the command was used. Updating does not extend it. Deleting the message uses the URL up. Files cannot be added through an update.
Show progress
Post a card with a loader, update its sub text as the job moves on, and end on the result. The loader is a spinner with a line and a smaller line under it (loader_text, loader_sub_text).
const { callback_url } = await post({
message_container: { color: 'blue', loader: true, loader_text: 'Deploying…', loader_sub_text: 'Step 1 of 3: building' }
});
const update = (body) => {
return fetch(callback_url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
};
await build();
await update({ message_container: { color: 'blue', loader: true, loader_text: 'Deploying…', loader_sub_text: 'Step 2 of 3: running migrations' } });
await migrate();
await update({
message_container: { color: 'green', title: 'Deploy complete', description: 'v2.1 is live on production.' },
actions: [{ type: 'url:https://ci.example.com/deploys/218', text: 'View logs', color: 'green' }]
});Delete a message
Send DELETE to the callback_url and the message is gone for everyone. The URL cannot be used again afterwards.
Listen to a message
A stream_url is a live stream (Server-Sent Events) of what happens on your message. Open it and events arrive as they happen, each as a JSON data: line whose type says what it is:
curl -N "$STREAM_URL"
data: {"type": "reaction", "message_id": "...", "emoji": ":tada:", "action": "add", "member_guid": "...", "member": {...}, "ts": 1790000000000}
data: {"type": "action", "message_id": "...", "action_id": "approve", "action": {"id": "approve", "payload": {"deploy": 218}, ...}, "member_guid": "...", "member": {...}, "ts": 1790000004200}
data: {"type": "reply", "message_id": "...", "content": "Ship it!", "member_guid": "...", "member": {...}, "ts": 1790000009800}
event: expired
data: {}| Event | When | Data |
|---|---|---|
action | A button was pressed. | action_id, and the stored button in action with its payload |
reaction | A reaction was added or removed. | emoji, and action: add, remove or removeall |
reply | Someone replied to the message. | The reply's content |
expired | The stream is closing. Sent as a named event. | None |
message_id, who did it (member_guid, member) and when (ts). A comment line every 20 seconds keeps the connection open.Listening in JavaScript
const events = new EventSource(streamUrl);
// Every event arrives as a plain message; its kind is in "type".
events.onmessage = (e) => {
const ev = JSON.parse(e.data);
if ((ev.type === 'action') && (ev.action_id === 'approve')) {
startDeploy(ev.action.payload.deploy);
}
};
// The one named event: the stream is closing.
events.addEventListener('expired', () => {
events.close();
});Where you get one
| Where it comes from | Open for |
|---|---|
| A webhook post with buttons | 10 minutes, or an hour with "sse_event_extended_timeout": true |
| Every command request | 10 minutes |
Errors
| Status | Code | Meaning |
|---|---|---|
401 | INVALID_TOKEN | The token in the URL is wrong. |
404 | NOT_FOUND | The stream expired or never existed. |