Przejdź do treści głównej
Developers Live updates

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

  1. Keep the callback URL

    Every webhook post and every command request comes with a callback_url for that message.

    bash
    BODY='{"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-..."}
  2. PUT to update

    Send the new state. The card changes in place for everyone.

    bash
    curl -X PUT "$CALLBACK_URL" -H "Content-Type: application/json" \
      -d '{"message_container": {"color": "green", "title": "Deploy complete", "description": "v2.1 is live."}}'
  3. DELETE to remove

    No body needed.

    bash
    curl -X DELETE "$CALLBACK_URL"

Update a message

PUT JSON to the callback_url. Send only what you want to change.

FieldTypeWhat it does
message_containerobjectThe new card. See message cards.
actionsarrayNew buttons. "actions": [] takes them all away; leaving the field out keeps them.
contentstringNew text.
title, description, color, loader, ...stringShorthand: card fields at the top level are wrapped into a card for you.
A new 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

StatusCodeMeaning
200{"success": true}Accepted. The update follows right after.
400MISSING_FIELDSNothing to update in the body.
400INVALID_BODYThe body is not valid JSON.
400a button codeSomething is wrong with a button, see buttons.
401INVALID_TOKENThe URL is not valid.
404TOKEN_NOT_FOUNDThe URL expired or was used to delete the message.
502PUBLISH_FAILEDThe 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).

javascript
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:

sse
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: {}
EventWhenData
actionA button was pressed.action_id, and the stored button in action with its payload
reactionA reaction was added or removed.emoji, and action: add, remove or removeall
replySomeone replied to the message.The reply's content
expiredThe stream is closing. Sent as a named event.None
Events are not stored for later. Open the stream as soon as you have the URL: what happens before you connect is not sent. Every event also carries the message_id, who did it (member_guid, member) and when (ts). A comment line every 20 seconds keeps the connection open.

Listening in JavaScript

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 fromOpen for
A webhook post with buttons10 minutes, or an hour with "sse_event_extended_timeout": true
Every command request10 minutes

Errors

StatusCodeMeaning
401INVALID_TOKENThe token in the URL is wrong.
404NOT_FOUNDThe stream expired or never existed.

Keep building