> ## Documentation Index
> Fetch the complete documentation index at: https://modem.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages

> Send the posts, comments, and replies inside a source container

A **message** is one post, comment, or reply inside a [source container](/docs/api/ingest/source-containers): a chat message in a channel, a customer's reply on a ticket, a comment on an issue. Messages are what Modem reads; source containers tell it how to read them.

Three event types manage messages: `message.created`, `message.updated`, and `message.deleted`. All use the standard [envelope](/docs/api/ingest/overview#event-envelope).

## The source container reference

Every message carries a reference to the source container it belongs to:

```json theme={null}
"source_container": {
    "kind": "support_thread",
    "external_id": "48213"
}
```

Modem resolves the reference using the message's `source_name` plus these two fields, so all three must match the source container's [identity](/docs/api/ingest/source-containers#identity) exactly.

The reference is what connects a message to processing. Messages are queued against their source container and drained in order, with the source container's kind, title, description, and lifecycle state as context. Without a matching source container:

* A message with a reference to a source container Modem has not seen yet is stored and waits. It is processed once the source container's `source_container.created` event arrives. A dangling reference is fine; it just delays processing.
* A message with no reference at all is stored but not processed.

<Warning>
  The `source_container` reference will soon be **required**. Messages without one are not processed today, and will be rejected with a
  `400` once the requirement is enforced.
</Warning>

A message's source container is set once. Re-sending the message with a different reference does not move it.

## Message payload

For `message.created` and `message.updated`, `body.data` looks like this:

```json theme={null}
{
    "data": {
        "source_name": "helphub",
        "source_message_id": "msg_8c41e",
        "source_message_type": "message",
        "created_at": "2026-03-09T15:30:00Z",
        "source_container": {
            "kind": "support_thread",
            "external_id": "48213"
        },
        "channel": {
            "source_name": "helphub",
            "server_name": "Acme (HelpHub)",
            "source_server_id": "acme",
            "channel_name": "Enterprise queue",
            "source_channel_id": "queue_enterprise"
        },
        "author": {
            "source_author_id": "cust_91b2",
            "display_name": "Priya Natarajan",
            "email": "priya@customer.example"
        },
        "content": "Still happening after clearing cookies. Every SSO user gets bounced back to the login page.",
        "source_url": "https://acme.helphub.example/tickets/48213#msg_8c41e"
    }
}
```

### Required fields

| Field                                | Notes                                                                                                                                          |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_name`                        | The source slug. Must match the source container's `source_name`                                                                               |
| `source_message_id`                  | Your tool's own stable ID for this message                                                                                                     |
| `source_message_type`                | Upstream message classification, usually `message`. Use `comment` for issue comments if your tool distinguishes them                           |
| `created_at`                         | When the message was written, from the source                                                                                                  |
| `source_container`                   | The [source container reference](#the-source-container-reference). Accepted without it today, but not processed; becoming required (see below) |
| `channel`                            | Where the message came from, see below                                                                                                         |
| `content`, `title`, or `attachments` | At least one must be present                                                                                                                   |

### `channel`

`channel` describes the broader place a message came from and is used for filtering and display. It is coarser than the source container.

| Source container kind | Suggested `channel`                                       |
| --------------------- | --------------------------------------------------------- |
| `chat_channel`        | The channel itself (`source_channel_id` = the channel ID) |
| `support_thread`      | The inbox, queue, or team the ticket lives in             |
| `issue`               | The project, board, or repository                         |
| `email_thread`        | The mailbox                                               |
| `feedback_submission` | The board or category                                     |

`channel.source_name` must equal `data.source_name`.

### `author`

`author` is optional, but a message without one contributes nothing to the people and companies Modem tracks. Send it whenever you can.

| Field               | Notes                                                                                                                                                                                                                                    |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_author_id`  | Stable upstream ID for the person. Modem derives the author's identity from it, so omit `author` entirely rather than falling back to a shared placeholder like `"unknown"`, which would merge every unidentified author into one person |
| `display_name`      |                                                                                                                                                                                                                                          |
| `username`          |                                                                                                                                                                                                                                          |
| `email`             | Pass it through when your tool provides it. Modem uses email to recognize the same person across sources                                                                                                                                 |
| `is_bot`            | `true` for automated authors (bots, integrations, your own app). Defaults to `false`                                                                                                                                                     |
| `source_avatar_url` |                                                                                                                                                                                                                                          |

<Note>
  Bot-authored content is filtered during extraction. If a bot relays human-written text (an alert feed that quotes a customer, a form
  that posts submissions), leave `is_bot` false or Modem will discard the content.
</Note>

### Optional fields

| Field                                        | Notes                                                                                                                    |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `title`                                      | Up to 500 characters. Useful for the first message in an issue or feedback submission                                    |
| `is_internal`                                | `true` for a note withheld from the external participant, such as a support agent's private comment. Defaults to `false` |
| `edited_at`                                  | When the message was last edited at the source                                                                           |
| `attachments`                                | Images are OCR'd and their text is included in extraction                                                                |
| `metadata`                                   | Free-form JSON. Prefer structured fields here over embedding JSON in `content`                                           |
| `source_url`                                 | Deep link to the message in your tool                                                                                    |
| `source_thread_id`, `source_thread_type`     | Threading inside the source container. See below                                                                         |
| `source_reply_to_id`, `source_reply_to_type` | The message this one replies to. See below                                                                               |

### Threading and replies

Source containers scope the conversation; threads and replies describe structure *inside* it.

* `source_thread_id` and `source_thread_type` identify the thread a message belongs to (a reply thread in a chat channel, for example). Modem treats distinct threads inside one source container as distinct concerns by default.
* `source_reply_to_id` and `source_reply_to_type` identify the specific message this one replies to.

Each pair must be sent together or omitted together. For kinds where the source container *is* the thread (`support_thread`, `issue`, `email_thread`), you can omit both.

## Examples

### `message.created`

The first message in a chat channel source container:

```json theme={null}
{
    "event_type": "message.created",
    "created_at": "2026-03-09T15:30:01Z",
    "client": { "name": "@acme/chatterbox-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "chatterbox",
            "source_message_id": "1709854200000",
            "source_message_type": "message",
            "created_at": "2026-03-09T15:30:00Z",
            "source_container": {
                "kind": "chat_channel",
                "external_id": "ws_7f3a:ch_support_eng"
            },
            "channel": {
                "source_name": "chatterbox",
                "server_name": "Acme",
                "source_server_id": "ws_7f3a",
                "channel_name": "support-eng",
                "source_channel_id": "ch_support_eng"
            },
            "author": {
                "source_author_id": "u_2b7d",
                "display_name": "Jordan Lee",
                "email": "jordan@acme.example"
            },
            "content": "HelpHub #48213: enterprise customer can't log in after enabling SSO. Anyone seen the IdP key rotation issue before?",
            "source_url": "https://acme.chatterbox.example/ws_7f3a/ch_support_eng/1709854200000"
        }
    }
}
```

A comment on an issue source container:

```json theme={null}
{
    "event_type": "message.created",
    "created_at": "2026-03-10T11:02:03Z",
    "client": { "name": "@acme/bugtrack-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "bugtrack",
            "source_message_id": "cmt_77a0",
            "source_message_type": "comment",
            "created_at": "2026-03-10T11:02:00Z",
            "source_container": {
                "kind": "issue",
                "external_id": "iss_01HXK4M9"
            },
            "channel": {
                "source_name": "bugtrack",
                "server_name": "Acme (BugTrack)",
                "source_server_id": "acme",
                "channel_name": "Platform",
                "source_channel_id": "proj_platform"
            },
            "author": {
                "source_author_id": "usr_8a90",
                "display_name": "Sam Okafor",
                "email": "sam@acme.example"
            },
            "content": "Root cause: we cache the IdP signing cert for 24h. Rotation invalidates every session until the cache expires. Fix is to honor the JWKS cache-control header.",
            "source_url": "https://acme.bugtrack.example/ACME-1187#cmt_77a0"
        }
    }
}
```

### `message.updated`

Same body as `message.created`, with the new content and an `edited_at`:

```json theme={null}
{
    "event_type": "message.updated",
    "created_at": "2026-03-09T15:45:05Z",
    "client": { "name": "@acme/chatterbox-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "chatterbox",
            "source_message_id": "1709854200000",
            "source_message_type": "message",
            "created_at": "2026-03-09T15:30:00Z",
            "edited_at": "2026-03-09T15:45:00Z",
            "source_container": {
                "kind": "chat_channel",
                "external_id": "ws_7f3a:ch_support_eng"
            },
            "channel": {
                "source_name": "chatterbox",
                "server_name": "Acme",
                "source_server_id": "ws_7f3a",
                "channel_name": "support-eng",
                "source_channel_id": "ch_support_eng"
            },
            "content": "HelpHub #48213: enterprise customer can't log in after enabling SSO. Started right after their IdP rotated its signing key."
        }
    }
}
```

### `message.deleted`

```json theme={null}
{
    "event_type": "message.deleted",
    "created_at": "2026-03-09T16:00:01Z",
    "client": { "name": "@acme/chatterbox-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "chatterbox",
            "source_message_id": "1709854200000",
            "source_message_type": "message",
            "deleted_at": "2026-03-09T16:00:00Z",
            "source_container": {
                "kind": "chat_channel",
                "external_id": "ws_7f3a:ch_support_eng"
            },
            "channel": {
                "source_name": "chatterbox",
                "server_name": "Acme",
                "source_server_id": "ws_7f3a",
                "channel_name": "support-eng",
                "source_channel_id": "ch_support_eng"
            }
        }
    }
}
```

## Processing behavior

* `message.created` stores the message, runs OCR on image attachments, and queues the message for processing in its source container. Modem drains each source container serially: messages are read in order, in windows, with earlier concerns from the same source container available so restatements are merged rather than duplicated.
* `message.updated` updates the stored content and OCRs newly added images. It does not re-run extraction for content that was already processed.
* `message.deleted` marks the message deleted and clears its content.

Source container lifecycle events feed the same pipeline. When a source container's state changes, that transition is recorded and used as evidence when Modem assesses the lifecycle of the topics the source container contributes to. See [Why state matters](/docs/api/ingest/source-containers#why-state-matters).

## Deduplication

<Warning>
  Most integrations should **not** set `source_nonce`. Only use it when your upstream provider has at-least-once delivery and may send you
  the same event more than once. If you construct events yourself, leave it empty.
</Warning>

`source_nonce` exists for one scenario: your upstream source gives you a **delivery ID** that is unique per delivery attempt, and that source may redeliver the same payload. Setting `source_nonce` to that delivery ID lets Modem drop the duplicate.

**Good example:** a webhook provider assigns every delivery a unique ID header and may redeliver the same webhook within seconds. Using that delivery ID as `source_nonce` is correct because it is scoped to the delivery, not to the underlying resource.

```json theme={null}
{
    "event_type": "message.created",
    "source_nonce": "a1f4b3c0-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
    "...": "..."
}
```

**Bad example:** building `source_nonce` from properties of the message (its ID, channel, or a combination). A `message.updated` with the same nonce as the original `message.created` would be silently dropped as a duplicate, and the update never applies.

```json theme={null}
// ❌ Do not do this. A future update to this message would be deduplicated away
{
    "event_type": "message.created",
    "source_nonce": "chatterbox:ch_support_eng:1709854200000",
    "...": "..."
}
```

### How it works

When `source_nonce` is present, Modem generates a deterministic event ID from:

```text theme={null}
organization_id + source_name + source_nonce
```

A second request with the same nonce produces the same event ID and is treated as a duplicate delivery. The nonce must identify the delivery, not the resource. Without `source_nonce`, each request gets a unique event ID, which is the correct behavior for most integrations.

The same applies to `source_container.*` events.

## Related

<CardGroup cols={2}>
  <Card title="Source Containers" icon="https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/label-alt-multiple.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=e7fbaf23c28396ac670b89cf38e1e4a6" href="/docs/api/ingest/source-containers" width="24" height="24" data-path="icons/label-alt-multiple.svg">
    Source container kinds, identity, lifecycle state, and the source container events.
  </Card>

  <Card title="Ingest API overview" icon="https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/code.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=6c9b5cbb0e89aebc55e9fd769945fc07" href="/docs/api/ingest/overview" width="24" height="24" data-path="icons/code.svg">
    Authentication, the event envelope, and the end-to-end protocol.
  </Card>
</CardGroup>
