Messages
Send the posts, comments, and replies inside a source container
A message is one post, comment, or reply inside a source container: 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.
The source container reference
Every message carries a reference to the source container it belongs to:
"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 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.createdevent arrives. A dangling reference is fine; it just delays processing. - A message with no reference at all is stored but not processed.
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:
{
"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. Accepted without it today, but not processed; becoming required (see below) |
channel |
Where the message came from, see below |
content |
The message text. May be an empty string if you send a non-empty title or at least one attachment |
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 |
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_idandsource_thread_typeidentify 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_idandsource_reply_to_typeidentify 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:
{
"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:
{
"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:
{
"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
{
"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.createdstores 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.updatedupdates the stored content and OCRs newly added images. It does not re-run extraction for content that was already processed.message.deletedmarks 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.
Deduplication
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.
{
"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.
// ❌ 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:
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.