---
title: "Live updates: change mssgs messages after posting"
description: "Update or delete an mssgs message after it was posted with its callback URL, show progress with a loader, and listen to replies, reactions and button presses on a live stream."
canonical: https://mss.gs/en/docs/live-updates
language: en
---

# 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 card** Change the text, the colour and the buttons, in place.

- **Show progress** A loader that moves through steps, then the result.

- **Delete it** Remove a message once it is no longer true.

- **Listen to it** Replies, reactions and button presses on your message, live.

In the app

Posted with a loader

Updated: step 2 of 3

#### Deploy complete

Updated: done, with a button

One message, updated twice through its callback URL. Nobody sees three messages, only one that changes.

- [Quick start](#quick-start)

- [Update](#update)

- [Show progress](#progress)

- [Delete](#delete)

- [Listen](#stream)

## Quick start

### Keep the callback URL

Every [webhook](https://mss.gs/en/docs/webhooks) post and every [command](https://mss.gs/en/docs/commands) 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-..."}
```

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

### 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.

| Field | Type | What it does |
| --- | --- | --- |
| message_container | object | The new card. See [message cards](https://mss.gs/en/docs/bots). |
| 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. |

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

```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: {}
```

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

### 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 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. |

## Keep building
