Skip to main content
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.
Only message events are supported in this first pass: message.created, message.updated, and message.deleted.

Authentication

Send your project key in the x-modem-public-key header.
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 OrganizationProjects.
  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.
Despite the name, treat this key like a server-side bearer credential. Do not ship it in browser code or mobile apps.

Endpoint

POST /ingest
Content-Type: application/json
Send a JSON event body with your project key in the x-modem-public-key header:
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.
FieldRequiredNotes
event_typeYesmessage.created, message.updated, or message.deleted
created_atYesISO 8601 timestamp for when your integration emitted the event
received_atNoISO 8601 timestamp for when you received it from the upstream source
source_nonceNoUpstream delivery ID for deduplication — see Deduplication before using
clientYesProducer metadata for the integration sending the event
bodyYesEvent payload

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:
{
  "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

FieldNotes
source_nameStable lowercase slug for the message source
source_message_idStable upstream message ID
source_message_typeStable upstream message classification, usually message
created_atMessage creation timestamp from the source
channelSource workspace and channel info
content, title, or attachmentsAt 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.

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

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

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

{
  "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.
{
  "success": true,
  "id": "0195789f-2a65-7c58-9c50-9dbf5fbc5f91",
  "message": "Event queued for processing",
  "details": {
    "textLength": 57,
    "sourceType": "microsoft-teams"
  }
}
Common error responses:
StatusWhen it happens
400Invalid JSON, invalid schema, or no extractable text content
401Missing or invalid project key
405Method other than POST
500Event 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

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.
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.
{
  "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.
// ❌ 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:
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.
  • There is no public OpenAPI endpoint yet.
  • Internal vs external producer provenance is not enforced by auth yet.
https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/link.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=e774d33588635d44c6c935b78ed55f13

Integrations

See Modem’s built-in integrations and how they capture data.
https://mintcdn.com/modem-844d7a4a/Wr2r4IRr97lNQiQb/icons/shield.svg?fit=max&auto=format&n=Wr2r4IRr97lNQiQb&q=85&s=ee1ca3093fcbdf45ec5dc1a74652bed6

Security & Privacy

Review the current security model for data sent to Modem.