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

# Ingest

> Send messages into Modem through the ingest API

The ingest API lets you send message events into Modem from your own integrations. This is a server-to-server API for developers who want to push messages from tools Modem does not natively connect to yet.

<Info>Only message events are supported in this first pass: `message.created`, `message.updated`, and `message.deleted`.</Info>

## Authentication

Send your project key in the `x-modem-public-key` header.

```http theme={null}
x-modem-public-key: modem_<32_hex_characters>
```

Project keys are managed per project, not per organization.

To get a key:

1. Open your organization in the Modem dashboard.
2. Go to **Organization** → **Projects**.
3. Open the project you want to ingest into.
4. In **Public Keys**, create a key or copy an existing active key.

Only org admins and owners can create, deactivate, or revoke keys.

<Note>Despite the name, treat this key like a server-side bearer credential. Do not ship it in browser code or mobile apps.</Note>

## Endpoint

```http theme={null}
POST /ingest
Content-Type: application/json
```

Send a JSON event body with your project key in the `x-modem-public-key` header:

```bash theme={null}
curl -X POST https://ingest.modem.dev/ingest \
  -H "Content-Type: application/json" \
  -H "x-modem-public-key: $MODEM_PUBLIC_KEY" \
  -d '{ ... }'
```

## Event Envelope

Every request sends a single event.

| Field          | Required | Notes                                                                                     |
| -------------- | -------- | ----------------------------------------------------------------------------------------- |
| `event_type`   | Yes      | `message.created`, `message.updated`, or `message.deleted`                                |
| `created_at`   | Yes      | ISO 8601 timestamp for when your integration emitted the event                            |
| `source_nonce` | No       | Upstream delivery ID for deduplication — see [Deduplication](#deduplication) before using |
| `client`       | Yes      | Producer metadata for the integration sending the event                                   |
| `body`         | Yes      | Event payload                                                                             |

Modem assigns `received_at` internally when the event is accepted and stored. Do not send `received_at` in the request body.

### `client`

`client.name` identifies the producer that submitted the event to Modem.

* Use a lowercase package-style identifier such as `@your-company/teams-sync`.
* `@modem/*` is reserved for Modem-managed integrations.
* `client.name` is descriptive producer metadata. It is not a trusted provenance boundary.

`client.version` must use semantic versioning, for example `1.0.0`.

## Message Payload

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

```json theme={null}
{
    "data": {
        "source_name": "microsoft-teams",
        "source_message_id": "1709854200000",
        "source_message_type": "message",
        "created_at": "2026-03-09T15:30:00Z",
        "channel": {
            "source_name": "microsoft-teams",
            "server_name": "Acme Workspace",
            "source_server_id": "workspace-123",
            "channel_name": "support",
            "source_channel_id": "channel-456"
        },
        "author": {
            "source_author_id": "user-789",
            "display_name": "Jordan Lee",
            "email": "jordan@example.com"
        },
        "content": "We are seeing login failures after the latest deploy.",
        "source_url": "https://teams.microsoft.com/l/message/1709854200000"
    }
}
```

### Required fields

| Field                                | Notes                                                     |
| ------------------------------------ | --------------------------------------------------------- |
| `source_name`                        | Stable lowercase slug for the message source              |
| `source_message_id`                  | Stable upstream message ID                                |
| `source_message_type`                | Stable upstream message classification, usually `message` |
| `created_at`                         | Message creation timestamp from the source                |
| `channel`                            | Source workspace and channel info                         |
| `content`, `title`, or `attachments` | At least one must be present                              |

### Important rules

* `channel.source_name` must exactly match `data.source_name`.
* `source_name` should stay stable within your organization. Modem uses it as part of deterministic IDs.
* Custom sources use plain slugs directly in `source_name`. Do not prefix them with `custom_`.
* Built-in values like `slack`, `discord`, and `github` remain reserved for those sources.
* `is_internal` is optional and defaults to `false`; send `true` for a message withheld from the external participant (such as an agent's internal note).

### Threading and replies

Use these optional pairs when you have them:

* `source_thread_id` and `source_thread_type`
* `source_reply_to_id` and `source_reply_to_type`

Each pair must be sent together or omitted together.

## Custom Sources

Custom message sources are supported for message events.

* Good `source_name` values: `microsoft-teams`, `intercom`, `zendesk-chat`
* Better when one upstream app needs multiple distinct feeds: `teams-support`, `teams-sales`
* Avoid renaming a source later by changing the slug. That creates a new source identity inside Modem.

`source_name` is the source identity. There is no separate `source_key`.

## Examples

### `message.created`

```json theme={null}
{
    "event_type": "message.created",
    "created_at": "2026-03-09T15:30:01Z",
    "client": {
        "name": "@your-company/teams-sync",
        "version": "1.0.0"
    },
    "body": {
        "data": {
            "source_name": "microsoft-teams",
            "source_message_id": "1709854200000",
            "source_message_type": "message",
            "created_at": "2026-03-09T15:30:00Z",
            "channel": {
                "source_name": "microsoft-teams",
                "server_name": "Acme Workspace",
                "source_server_id": "workspace-123",
                "channel_name": "support",
                "source_channel_id": "channel-456"
            },
            "author": {
                "source_author_id": "user-789",
                "display_name": "Jordan Lee",
                "email": "jordan@example.com"
            },
            "content": "We are seeing login failures after the latest deploy.",
            "source_url": "https://teams.microsoft.com/l/message/1709854200000"
        }
    }
}
```

### `message.updated`

```json theme={null}
{
    "event_type": "message.updated",
    "created_at": "2026-03-09T15:45:05Z",
    "client": {
        "name": "@your-company/teams-sync",
        "version": "1.0.0"
    },
    "body": {
        "data": {
            "source_name": "microsoft-teams",
            "source_message_id": "1709854200000",
            "source_message_type": "message",
            "created_at": "2026-03-09T15:30:00Z",
            "edited_at": "2026-03-09T15:45:00Z",
            "channel": {
                "source_name": "microsoft-teams",
                "server_name": "Acme Workspace",
                "source_server_id": "workspace-123",
                "channel_name": "support",
                "source_channel_id": "channel-456"
            },
            "content": "We are seeing login failures after the latest deploy. It started after build 412."
        }
    }
}
```

### `message.deleted`

```json theme={null}
{
    "event_type": "message.deleted",
    "created_at": "2026-03-09T16:00:01Z",
    "client": {
        "name": "@your-company/teams-sync",
        "version": "1.0.0"
    },
    "body": {
        "data": {
            "source_name": "microsoft-teams",
            "source_message_id": "1709854200000",
            "source_message_type": "message",
            "deleted_at": "2026-03-09T16:00:00Z",
            "channel": {
                "source_name": "microsoft-teams",
                "server_name": "Acme Workspace",
                "source_server_id": "workspace-123",
                "channel_name": "support",
                "source_channel_id": "channel-456"
            }
        }
    }
}
```

## Responses

Successful requests return `200 OK`.

```json theme={null}
{
    "success": true,
    "id": "0195789f-2a65-7c58-9c50-9dbf5fbc5f91",
    "message": "Event queued for processing",
    "details": {
        "textLength": 57,
        "sourceType": "microsoft-teams"
    }
}
```

Common error responses:

| Status | When it happens                                              |
| ------ | ------------------------------------------------------------ |
| `400`  | Invalid JSON, invalid schema, or no extractable text content |
| `401`  | Missing or invalid project key                               |
| `405`  | Method other than `POST`                                     |
| `500`  | Event could not be queued                                    |

## Processing Behavior

* `message.created` stores the message, runs attachment OCR when needed, then sends the message into grouping and topic processing.
* `message.updated` updates the stored message and can OCR newly added image attachments. It does not re-run grouping or topic generation for existing conversations.
* `message.deleted` marks the stored message deleted and clears message content.

## Deduplication

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

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

**Good example — GitHub webhooks:** GitHub assigns every webhook delivery an `X-GitHub-Delivery` header. GitHub may redeliver the same webhook multiple times 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 — constructing a nonce from message properties:** If you build a `source_nonce` from characteristics of the message itself (such as `source_message_id`, channel ID, or a combination of fields), you will block all future events that touch that message. A `message.updated` event with the same nonce as the original `message.created` would be silently dropped as a duplicate — the update never gets applied.

```json theme={null}
// ❌ Do not do this — a future update to this message would be deduplicated away
{
    "event_type": "message.created",
    "source_nonce": "microsoft-teams:channel-456: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 of the same event. This means **the nonce must identify the delivery, not the resource**. If you omit `source_nonce`, each request gets a unique event ID and Modem treats it as a new delivery — which is the correct behavior for most integrations.

## Current Limits

* Public ingest is message-only in this version.
* Pull request and issue event families remain built-in-only.
* This ingest endpoint is not yet part of the OpenAPI spec; the rest of the public API is documented under [API Reference](/docs/api-reference/introduction).
* Internal vs external producer provenance is not enforced by auth yet.

## Related

<CardGroup cols={2}>
  <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">
    See Modem's built-in integrations and how they capture data.
  </Card>

  <Card title="Security & Privacy" icon="https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/shield.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=ee1ca3093fcbdf45ec5dc1a74652bed6" href="/docs/features/security-privacy" width="24" height="24" data-path="icons/shield.svg">
    Review the current security model for data sent to Modem.
  </Card>
</CardGroup>
