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

# Source Containers

> The unit of conversation Modem processes: channels, tickets, issues, threads

A **source container** is one place where a conversation happens in one of your tools: a chat channel, a support ticket, a tracked issue, an email thread. Every [message](/docs/api/ingest/messages) you send belongs to exactly one source container, and Modem processes messages source container by source container.

Source containers are how Modem aggregates across tools. A customer complaint in a support ticket, the issue your team filed for it, and the channel where engineers discussed the fix are three source containers from three sources. Modem reads each one on its own terms (a ticket has a status, a channel does not) and then connects what it finds into a single topic.

## Why source containers matter

Before you send a single message, Modem needs to know what the source container is. The source container does four jobs:

| Job                     | What the source container provides                                                                                                                                                                                                                         |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Triggers processing** | Messages are queued against the source container they reference. Modem drains each source container serially, in message order, and nothing in a source container is processed until the source container exists.                                          |
| **Sets the scope**      | Messages in the same source container are read together; messages in different source containers are not. Modem deduplicates restated concerns *within* a source container, so how you scope source containers decides what gets read as one conversation. |
| **Declares the kind**   | The `kind` tells Modem what the content means. A support thread is a customer stating a problem. An issue is your team's restatement of one. Modem applies different extraction and topic rules to each.                                                   |
| **Carries lifecycle**   | For kinds that have one, the source container's `state` (open, in progress, done, ...) and its transition history are evidence Modem uses to judge whether the work behind a topic is still open, in progress, completed, or dismissed.                    |

## Identity

A source container is identified by three strings:

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

| Field         | Rule                                                                                                                                          |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_name` | The source's slug, the same value you put on its messages. See [Choosing a `source_name`](/docs/api/ingest/overview#choosing-a-source_name).       |
| `kind`        | One of the [source container kinds](#source-container-kinds). Frozen once chosen.                                                             |
| `external_id` | A string you choose that is stable for the life of the source container. Usually the tool's own ID for the channel, ticket, issue, or thread. |

Modem derives the source container's internal ID from these three values plus your organization. The same triple always resolves to the same source container, which is what makes re-sending a source container snapshot safe and what lets a message find its source container.

<Tip>
  **`external_id` sets the scope.** Whatever you compute as `external_id` decides what counts as one conversation. One source container
  per help-desk ticket means each ticket is read on its own. One source container per help-desk *queue* would read every ticket in the
  queue as one endless conversation. Pick the tool's natural unit (the thing a person would open in a browser and say "this
  conversation"), and be consistent. You can change the scope later by changing what you compute, but existing messages stay on the source
  containers they were sent to.
</Tip>

Make `external_id` byte-for-byte identical everywhere you use it. `"48213"` and `"ticket-48213"` are two different source containers.

## Source container kinds

`kind` is a policy key, not a label. It decides how Modem extracts content from the source container's messages, how that content participates in topics, and which lens the source container appears under in the Sources view.

| `kind`                | Use it for                                                             | One source container per | Lifecycle state | Available on the ingest API |
| --------------------- | ---------------------------------------------------------------------- | ------------------------ | --------------- | --------------------------- |
| `chat_channel`        | Chat rooms, channels, alert and notification feeds, any ongoing stream | Channel or stream        | None            | Yes                         |
| `support_thread`      | Help-desk tickets, customer chat conversations, two-way support email  | Ticket or conversation   | Yes             | Yes                         |
| `issue`               | Issue trackers, bug databases, project boards                          | Issue                    | Yes             | Yes                         |
| `email_thread`        | Mailbox threads                                                        | Thread root              | None            | Yes                         |
| `feedback_submission` | Public feedback boards, in-app feedback forms, feature request portals | Submission               | Optional        | Yes                         |
| `pull_request`        | Code review: pull requests, merge requests                             | Pull request             | Yes             | Built-in integrations only  |
| `transcript`          | Recorded calls and meetings                                            | Call                     | None            | Built-in integrations only  |
| `social_feed`         | Public social media: a saved search, a subreddit, a hashtag            | Saved search or feed     | None            | Built-in integrations only  |

<Note>
  **Two kinds are gated when it comes to starting a topic.** `issue` and `pull_request` are tracking artifacts: your team's restatement of
  a concern rather than the concern itself. Content from these kinds can always *join* an existing topic, but it only creates a *new*
  topic on its own when someone outside your organization took part in it. A `pull_request` additionally can never anchor a topic by
  itself. Every other kind seeds topics freely. If you are choosing between `issue` and `support_thread` for something in between (a
  customer-visible bug portal, say), this is the difference that matters.
</Note>

The examples below use fictitious products (ChatterBox, HelpHub, BugTrack, MailRoom, VoiceBox) so the shape is clear without implying that a specific real product must be modeled a particular way. If your tool looks like one of these, model it the same way.

### `chat_channel`

An ongoing stream of messages with no beginning or end: a channel in your team chat, a shared room with a customer, an alerts feed a bot posts into. The conversation *is* the signal. People raise concerns directly, and Modem reads the messages as the concern itself rather than as a report about one.

A chat channel has no lifecycle. Send `state: null`. If your tool can archive channels, report that with `archived_at`.

Scope: one source container per channel. If your tool has workspaces or servers, put both IDs in `external_id` so channels with the same ID in different workspaces stay distinct.

```json theme={null}
{
    "event_type": "source_container.created",
    "created_at": "2026-03-09T15:30:01Z",
    "client": { "name": "@acme/chatterbox-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "chatterbox",
            "source_container": {
                "kind": "chat_channel",
                "external_id": "ws_7f3a:ch_support_eng",
                "parent": null,
                "human_key": null,
                "title": "#support-eng",
                "description": "Escalations from the support team to engineering. One thread per customer issue.",
                "state": null,
                "state_changed_at": null,
                "archived_at": null,
                "author": null,
                "external_url": "https://acme.chatterbox.example/ws_7f3a/ch_support_eng",
                "external_created_at": "2024-06-11T09:00:00Z",
                "external_updated_at": null,
                "closed_at": null,
                "metadata": { "workspace": "ws_7f3a", "visibility": "private" }
            }
        }
    }
}
```

<Tip>
  `description` is rendered as context when Modem extracts concerns from the channel's messages. A real sentence about what the channel is
  for ("escalations from support to engineering") measurably improves extraction on alert feeds and other channels whose messages do not
  explain themselves.
</Tip>

### `support_thread`

A two-way conversation between one customer and your team about one problem: a help-desk ticket, an in-app chat conversation, a support email exchange. The customer states the concern in their own words, so Modem reads the thread as the concern itself.

A support thread has a lifecycle. The tool's status maps onto [Modem's states](#lifecycle-state), and the transition to `done` is strong evidence that the concern was resolved.

Scope: one source container per ticket or conversation.

```json theme={null}
{
    "event_type": "source_container.created",
    "created_at": "2026-03-09T15:30:01Z",
    "client": { "name": "@acme/helphub-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "helphub",
            "source_container": {
                "kind": "support_thread",
                "external_id": "48213",
                "parent": null,
                "human_key": "#48213",
                "title": "Can't log in after enabling SSO",
                "description": null,
                "state": "waiting",
                "state_changed_at": "2026-03-09T15:22:40Z",
                "archived_at": null,
                "author": {
                    "source_author_id": "cust_91b2",
                    "display_name": "Priya Natarajan",
                    "email": "priya@customer.example"
                },
                "external_url": "https://acme.helphub.example/tickets/48213",
                "external_created_at": "2026-03-09T14:02:11Z",
                "external_updated_at": "2026-03-09T15:22:40Z",
                "closed_at": null,
                "metadata": { "priority": "high", "queue": "enterprise" }
            },
            "state_observation_source": "authoritative_snapshot",
            "state_changed_at_kind": "provider_timestamp",
            "external_state": { "status": "pending", "assignee": "agent_204" }
        }
    }
}
```

`author` is the person who opened the ticket. `human_key` is the short label people use to refer to it.

### `issue`

A tracked work item with workflow state: a bug, a task, a card on a project board. Issues are usually your team's restatement of a concern that was first raised somewhere else (a support thread, a chat message), so Modem treats them as **tracking artifacts**: an issue joins a topic readily, but it only seeds a *new* topic on its own when someone outside your organization took part in it. This keeps internal task churn from minting topics that no customer ever raised.

An issue has a lifecycle, and its lifecycle is the most informative of any kind. When the issue moves to `done`, Modem has direct evidence that the work behind the topic is complete.

Scope: one source container per issue.

```json theme={null}
{
    "event_type": "source_container.created",
    "created_at": "2026-03-10T10:15:00Z",
    "client": { "name": "@acme/bugtrack-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "bugtrack",
            "source_container": {
                "kind": "issue",
                "external_id": "iss_01HXK4M9",
                "parent": null,
                "human_key": "ACME-1187",
                "title": "SSO login loop when IdP rotates signing key",
                "description": "Users on SSO get bounced back to the login page after the IdP rotates its signing key. Reported via helphub #48213.",
                "state": "in_progress",
                "state_changed_at": "2026-03-10T10:14:02Z",
                "archived_at": null,
                "author": {
                    "source_author_id": "usr_3c1e",
                    "display_name": "Dana Whitfield",
                    "email": "dana@acme.example"
                },
                "external_url": "https://acme.bugtrack.example/ACME-1187",
                "external_created_at": "2026-03-09T16:40:00Z",
                "external_updated_at": "2026-03-10T10:14:02Z",
                "closed_at": null,
                "metadata": { "labels": ["auth", "sso"], "project": "platform" }
            },
            "state_observation_source": "explicit_lifecycle",
            "state_changed_at_kind": "provider_timestamp",
            "external_state": { "status": "In Progress", "assignee": "usr_8a90" }
        }
    }
}
```

Use `external_id` for the tool's immutable ID and `human_key` for the human-readable key. Keys like `ACME-1187` can change when an issue moves between projects; internal IDs do not.

### `email_thread`

A thread in a mailbox, keyed on the root message. Replies often quote earlier messages verbatim, and Modem accounts for that when it reads the thread. Like a chat channel, a mail thread has no lifecycle: send `state: null`.

Scope: one source container per thread. Use the root message's `Message-ID` header (or your mail provider's thread ID) as `external_id`, and send the same source container snapshot from every message in the thread. Modem keeps whichever snapshot it observed most recently, so a snapshot derived from a reply is fine as long as the fields you fill are ones every message in the thread agrees on (the subject with `Re:`/`Fwd:` stripped, for example).

```json theme={null}
{
    "event_type": "source_container.created",
    "created_at": "2026-03-11T08:05:00Z",
    "client": { "name": "@acme/mailroom-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "mailroom",
            "source_container": {
                "kind": "email_thread",
                "external_id": "<CAF3x9k2@mail.customer.example>",
                "parent": null,
                "human_key": null,
                "title": "Renewal pricing for the Enterprise plan",
                "description": null,
                "state": null,
                "state_changed_at": null,
                "author": null,
                "external_url": null,
                "external_created_at": null,
                "external_updated_at": "2026-03-11T08:04:31Z",
                "closed_at": null,
                "metadata": { "mailbox": "sales@acme.example" }
            }
        }
    }
}
```

`author` and `external_created_at` belong to the thread's root message. If the message you are looking at is a reply, you do not know them; send `null` rather than the reply's values.

### `feedback_submission`

Something a customer wrote unprompted that stands on its own: a post on a public feedback board, a feature request submitted through a form, an in-app "send feedback" entry. Nobody is obliged to reply, which is what distinguishes it from a support thread. Where the tool supports voting or comments, that activity reads as *demand* for the request rather than as a back-and-forth about it.

Lifecycle is optional. Many feedback boards have a status (under review, planned, shipped); if yours does, map it. If it does not, send `state: null`.

Scope: one source container per submission.

```json theme={null}
{
    "event_type": "source_container.created",
    "created_at": "2026-03-12T13:00:00Z",
    "client": { "name": "@acme/voicebox-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "voicebox",
            "source_container": {
                "kind": "feedback_submission",
                "external_id": "post_5d2a7",
                "parent": null,
                "human_key": null,
                "title": "Let admins force re-authentication after an SSO config change",
                "description": null,
                "state": "open",
                "state_changed_at": null,
                "archived_at": null,
                "author": {
                    "source_author_id": "u_44f1",
                    "display_name": "Marcus Oyelaran"
                },
                "external_url": "https://acme.voicebox.example/p/post_5d2a7",
                "external_created_at": "2026-03-12T12:41:00Z",
                "external_updated_at": "2026-03-12T12:41:00Z",
                "closed_at": null,
                "metadata": { "votes": 37, "board": "authentication" }
            },
            "state_observation_source": "authoritative_snapshot"
        }
    }
}
```

The submission's own text is a message. Send the post body as the first `message.created` in the source container, then each comment as a further message.

### Built-in-only kinds

Three kinds are produced by Modem's built-in integrations and are not yet accepted through the ingest API. They are listed so the full model is visible.

| `kind`         | What it is                                                                                                                                                                                                                                                      |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pull_request` | An implementation artifact addressing some concern. It reports on work rather than raising it, so a pull request can join a topic but cannot anchor one alone. Lifecycle: `in_progress` while open, `done` when merged, `canceled` when closed without merging. |
| `transcript`   | A recorded call or meeting. A live spoken conversation, read as the concern itself. Born complete, no lifecycle.                                                                                                                                                |
| `social_feed`  | A public social feed such as a saved search. There is one source container per *query*, not per conversation, because duplicates on social media cross conversation boundaries. No lifecycle.                                                                   |

## Lifecycle state

Kinds that track work (`support_thread`, `issue`, `feedback_submission`, `pull_request`) carry a `state`. Your tool has its own vocabulary; Modem normalizes it to five values.

| `state`       | Meaning                                                                                   |
| ------------- | ----------------------------------------------------------------------------------------- |
| `open`        | Exists, nobody has started on it                                                          |
| `in_progress` | Someone on your side is actively working on it or has responded                           |
| `waiting`     | Blocked on someone else, usually the customer or reporter                                 |
| `done`        | Resolved or completed                                                                     |
| `canceled`    | Closed without being resolved: won't fix, duplicate, spam, merged into another item       |
| `null`        | This source container has no lifecycle (chat channels, email threads, transcripts, feeds) |

### Mapping your tool's statuses

Map each native status to the closest Modem state. The mapping does not need to be lossless; put the native status in `external_state` if you want it preserved.

| Tool           | Native status                          | Modem `state` |
| -------------- | -------------------------------------- | ------------- |
| Help desk      | New, Unassigned                        | `open`        |
|                | Open (assigned), Agent replied         | `in_progress` |
|                | Pending, On hold, Awaiting customer    | `waiting`     |
|                | Solved, Resolved, Closed               | `done`        |
|                | Merged, Spam, Deleted                  | `canceled`    |
| Issue tracker  | Backlog, Triage, Todo                  | `open`        |
|                | In progress, In review, QA             | `in_progress` |
|                | Blocked, Needs info                    | `waiting`     |
|                | Done, Shipped, Released                | `done`        |
|                | Canceled, Won't fix, Duplicate         | `canceled`    |
| Feedback board | Open, Under review, Gathering feedback | `open`        |
|                | Planned, In progress, Building         | `in_progress` |
|                | Shipped, Complete                      | `done`        |
|                | Closed, Declined, Not planned          | `canceled`    |

A few judgment calls:

* **A ticket the customer has replied to** after an agent response usually goes back to `open` or `in_progress`, depending on whether the agent is still assigned.
* **Reopened** items go back to whatever state fits; Modem records every transition, so a `done` → `open` → `done` history is fine.
* **Closed vs. resolved.** If your tool distinguishes them and both mean "the work is finished", map both to `done`. `canceled` is for items that ended *without* resolution.
* **If you cannot tell**, prefer `open` over inventing precision. A wrong `done` is worse than a vague `open`.

### Why state matters

Source container state is the most direct lifecycle evidence Modem has. When Modem assesses whether a topic is open, in progress, completed, or dismissed, it reads the current state and transition history of every source container that contributes to the topic. A ticket moving to `done`, or an issue moving to `canceled`, changes that assessment. Conversation content alone ("we shipped it yesterday") is also evidence, but a status change is unambiguous.

This is why state is unimportant for a chat channel and very important for a ticket. Nobody "closes" a channel when a problem is fixed, so a channel's messages carry the resolution signal in their content. A ticket's status is the resolution signal, and if you send `state: null` for every ticket, Modem loses it.

### State fields

When `state` is non-null, describe how you learned it.

| Field                      | Required                     | Notes                                                                                                                                                                                                         |
| -------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state_observation_source` | Yes when `state` is non-null | How you learned the state. Must be **absent** when `state` is `null`. See the values below                                                                                                                    |
| `state_changed_at`         | Nullable                     | When the source container entered its current state, according to your tool. `null` if the tool does not say                                                                                                  |
| `state_changed_at_kind`    | Optional                     | `provider_timestamp` when `state_changed_at` came from the tool; `lifecycle_observation` when it is your own observation time standing in for one. Defaults to `provider_timestamp`                           |
| `closed_at`                | Nullable                     | When the source container reached `done` or `canceled`. `null` while it is open                                                                                                                               |
| `external_state`           | Optional                     | A small JSON object with the tool's native status fields, preserved alongside the transition. Defaults to `{ "state": <state> }`                                                                              |
| `archived_at`              | Optional, nullable           | Archive is separate from lifecycle. **Omit** the field if your tool has no archive concept; send `null` for "not archived"; send a timestamp for "archived". An omitted field never overwrites a stored value |

`state_observation_source` values, from most to least authoritative:

| Value                    | Use it when                                                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `explicit_lifecycle`     | A lifecycle event told you the state changed: a `ticket.solved` webhook, an `issue.closed` notification                                     |
| `authoritative_snapshot` | You read the current record from the tool's API, or a webhook delivered the full current object                                             |
| `api_backfill`           | You are importing historical records in bulk                                                                                                |
| `structured_fallback`    | You inferred the state from structured fields that are not the status itself (a `resolved_at` timestamp with no status field, for instance) |
| `message_fallback`       | You inferred the state from message content                                                                                                 |

When two observations of the same source container arrive with the same `created_at`, the more authoritative source wins. Otherwise the most recently observed snapshot wins. See [Idempotency](#idempotency).

## Events

Three event types manage source containers. All use the standard [envelope](/docs/api/ingest/overview#event-envelope).

### `source_container.created`

Stands up a source container. `body.data` carries a **complete snapshot**; every field is present, with `null` for unknown values.

| Field                                  | Type                            | Notes                                                                                                                                                                                     |
| -------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_name`                          | string                          | The source slug. Must match the `source_name` on the source container's messages                                                                                                          |
| `source_container.kind`                | string                          | One of the [kinds](#source-container-kinds) available on the ingest API                                                                                                                   |
| `source_container.external_id`         | string                          | The source container's stable identity. See [Identity](#identity)                                                                                                                         |
| `source_container.parent`              | `{kind, external_id}` or `null` | Reserved for nesting (a thread inside a channel). Send `null`                                                                                                                             |
| `source_container.human_key`           | string or `null`                | The short label people use: `#48213`, `ACME-1187`                                                                                                                                         |
| `source_container.title`               | string or `null`                | Rendered as extraction context. Populate it                                                                                                                                               |
| `source_container.description`         | string or `null`                | Rendered as extraction context. Populate it for channels and feeds, whose messages rarely explain what the source container is for                                                        |
| `source_container.state`               | enum or `null`                  | See [Lifecycle state](#lifecycle-state)                                                                                                                                                   |
| `source_container.state_changed_at`    | ISO 8601 or `null`              |                                                                                                                                                                                           |
| `source_container.archived_at`         | ISO 8601 or `null`, optional    | See [State fields](#state-fields)                                                                                                                                                         |
| `source_container.author`              | author or `null`                | Who opened the source container. Same shape as a [message author](/docs/api/ingest/messages#author). Send `null` if unknown; within the object, omit unknown fields rather than sending `null` |
| `source_container.external_url`        | URL or `null`                   | Deep link into your tool                                                                                                                                                                  |
| `source_container.external_created_at` | ISO 8601 or `null`              | When the source container was created in your tool                                                                                                                                        |
| `source_container.external_updated_at` | ISO 8601 or `null`              | When it last changed in your tool                                                                                                                                                         |
| `source_container.closed_at`           | ISO 8601 or `null`              |                                                                                                                                                                                           |
| `source_container.metadata`            | object                          | Free-form JSON. Send `{}` if you have nothing                                                                                                                                             |
| `state_observation_source`             | enum                            | Required when `state` is non-null; forbidden when `state` is `null`                                                                                                                       |
| `state_changed_at_kind`                | enum, optional                  |                                                                                                                                                                                           |
| `external_state`                       | object, optional                |                                                                                                                                                                                           |
| `provider_payload`                     | object, optional                | The raw record from your tool, if you want Modem to retain it. Scrubbed for PII before storage                                                                                            |
| `provider_payload_source`              | string, optional                | Where `provider_payload` came from (`webhook`, `rest_api`, ...)                                                                                                                           |

### `source_container.updated`

Identical body to `source_container.created`. Send it whenever the source container changes in your tool: a status transition, a retitle, an archive. Always send the full snapshot, not a diff.

The two event types differ in exactly one way: a `source_container.created` observed after a deletion revives the source container, and a `source_container.updated` never does (see below). Otherwise they are interchangeable, and if you are not sure whether Modem has seen a source container, sending `source_container.created` again is safe.

### `source_container.deleted`

Tombstones a source container. Once deleted, the source container's messages are no longer processed.

```json theme={null}
{
    "event_type": "source_container.deleted",
    "created_at": "2026-04-01T09:00:00Z",
    "client": { "name": "@acme/helphub-sync", "version": "1.0.0" },
    "body": {
        "data": {
            "source_name": "helphub",
            "source_container": {
                "kind": "support_thread",
                "external_id": "48213"
            },
            "deleted_at": "2026-04-01T08:59:12Z"
        }
    }
}
```

`deleted_at` is when the source container was removed in your tool. Send a deletion only for an actual removal. A ticket that was closed or merged is a state change (`done` or `canceled`), not a deletion.

A deletion is a tombstone, not a purge. The row and its messages remain, and a `source_container.created` event observed **after** the deletion revives it: `deleted_at` is cleared, the new snapshot is stored, and processing resumes. `source_container.updated` does not revive; it updates the snapshot but leaves the source container deleted.

If the first event Modem ever sees for an identity is a deletion, it records a bare tombstone (identity and `deleted_at`, nothing else). Messages referencing it wait as they would for any missing source container, and a later `source_container.created` fills it in and starts processing.

## Idempotency

Source container events are upserts keyed on the source container's [identity](#identity). Re-sending a snapshot is always safe. Modem keeps the snapshot with the most recent `created_at`, so:

* A retry of the same event is a no-op.
* Two events with different `created_at` resolve to the later one, regardless of arrival order. An out-of-order delivery of an older snapshot cannot overwrite a newer one.
* Two events with the same `created_at` resolve by `state_observation_source` rank, then by event ID.

This is why `created_at` must be your observation time and never later than the moment you looked. A bulk import stamped with the import time would outrank every live observation that came before it.

State transitions are additionally recorded as history. A source container that moved `open` → `in_progress` → `done` keeps all three facts even though only `done` is current, and that history is part of the lifecycle evidence.

## Ordering and failure modes

The source container gates the **start** of processing, not the acceptance of messages. A message that references a source container Modem has not seen yet is stored with its reference intact and waits. Once the source container's `source_container.created` event lands, the queued messages are processed in order. Sending the source container first is the natural order, but a message arriving before it is not an error.

Other rules:

* **The reference must match exactly.** `source_name`, `kind`, and `external_id` on a message must equal the source container's byte for byte. A mismatch is a reference to a different (probably nonexistent) source container, so the message waits for a source container that never arrives.
* **The reference will soon be required.** Messages without a `source_container` reference are stored but not processed today. Once the requirement is enforced they will be rejected with a `400`. See [The source container reference](/docs/api/ingest/messages#the-source-container-reference).
* **A message's source container is fixed** once it is set. Sending the same message again with a different reference does not move it.
* **`kind` is validated.** A `kind` outside the [available set](#source-container-kinds) returns `400`.
* **One source container event per source container**, plus one per lifecycle change. Do not send a source container snapshot alongside every message. Re-sending is safe, but it is wasted work and counts toward usage.

## Related

<CardGroup cols={2}>
  <Card title="Messages" icon="https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/mail.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=734a974951595d9c5b50b15bc65a73d1" href="/docs/api/ingest/messages" width="24" height="24" data-path="icons/mail.svg">
    The message payload and how a message references its source container.
  </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>

  <Card title="Topics" icon="https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/label-alt-multiple.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=e7fbaf23c28396ac670b89cf38e1e4a6" href="/docs/guides/topics" width="24" height="24" data-path="icons/label-alt-multiple.svg">
    What Modem builds from the source containers and messages you send.
  </Card>

  <Card title="Integrations" icon="https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/link.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=e774d33588635d44c6c935b78ed55f13" href="/docs/integrations/overview" width="24" height="24" data-path="icons/link.svg">
    Built-in integrations, which model their sources the same way.
  </Card>
</CardGroup>
