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

# Ingestion API

> HTTP reference for the Moda Ingestion API: authentication, POST /v1/ingest, POST /v1/ingest/multi, and POST /v1/traces.

The Ingestion API accepts conversation data over plain HTTP. Use it when your language has no Moda SDK, when data originates outside your application process (backfills, message queues, webhook consumers), or when you need channel formats such as email and call transcripts.

## Base URL

```
https://moda-ingest.modas.workers.dev
```

All endpoints live under this host. There is no path-level versioning beyond the `/v1` prefix.

## Authentication

Every endpoint except `GET /health` requires an API key (called an ingestion key in the dashboard) in the `Authorization` header:

```
Authorization: Bearer moda_sk_...
```

Keys are created at **Settings → Ingestion keys** and are shown once at creation. A key is bound to your tenant and cannot be re-pointed. See [Authentication](/administration/authentication) for key management and revocation.

Requests with a missing or invalid key return `401`. The `message` field names the failure — `Missing Authorization header` when the header is absent, `Invalid or expired API key` otherwise:

```json theme={"dark"}
{
  "success": false,
  "count": 0,
  "message": "Invalid or expired API key",
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
```

<Note>
  The `Bearer ` prefix is optional: a bare key in the `Authorization` header is also accepted. CLI session tokens are not valid here — only `moda_sk_` API keys.
</Note>

## Request IDs

You can send an optional `X-Request-ID` header on any request. If the value is a valid UUID, it is echoed back as `requestId` in the JSON response; otherwise the server generates a random UUID. Use it to correlate requests, responses, and retries in your own logs.

```bash theme={"dark"}
curl https://moda-ingest.modas.workers.dev/v1/ingest \
  -H "Authorization: Bearer YOUR_MODA_API_KEY" \
  -H "X-Request-ID: 3f1c9a52-8f6e-4b0d-9c3a-2d7e5b1a4c88" \
  -H "Content-Type: application/json" \
  -d '{"events": []}'
```

## GET /health

Health check. No authentication. Returns `200` with the plain-text body `ok`.

```bash theme={"dark"}
curl https://moda-ingest.modas.workers.dev/health
```

```
ok
```

## POST /v1/ingest

Sends conversation events as simple JSON. Each event is one message in a conversation. Events with the same `conversation_id` are grouped into one conversation.

### Request body

<ParamField body="environment" type="string">
  Environment for all events in the request. Must be exactly `development`, `staging`, or `production` (defaults to `production`); any other value is rejected with `400`. Individual events can override it with their own `environment` field.
</ParamField>

<ParamField body="events" type="array">
  Up to 1,000 events per request. An empty or absent array returns `200` with `count: 0` and the message `No events provided`.
</ParamField>

### Event fields

<ParamField body="conversation_id" type="string" required>
  ID of the conversation this event belongs to. Events sharing a `conversation_id` form one conversation.
</ParamField>

<ParamField body="role" type="string" required>
  Who produced the message — typically `user`, `assistant`, or `system`. Events with role `assistant` are recorded as model output; all other roles are recorded as input to the model.
</ParamField>

<ParamField body="message" type="string">
  The message text. Used when `content` and `content_blocks` are absent.
</ParamField>

<ParamField body="content" type="string | array">
  The message content, either as a string or as an array of content blocks (see below). If both `content` and `message` are present, `content` takes precedence.
</ParamField>

<ParamField body="content_blocks" type="array">
  Structured content blocks for tool use, extended thinking, or images (see below). Used when `content` is absent.
</ParamField>

<ParamField body="timestamp" type="string">
  ISO 8601 date-time of the event, for example `2026-08-16T10:30:00Z`. Defaults to the time the event is received. A `timestamp` that is present but unparseable is rejected with `400`.
</ParamField>

<ParamField body="trace_id" type="string">
  Groups related events under one trace. Defaults to `conversation_id`.
</ParamField>

<ParamField body="user_id" type="string">
  Identifier for the end user of the conversation.
</ParamField>

<ParamField body="input_tokens" type="number">
  Prompt (input) tokens used by the generation. Assistant events.
</ParamField>

<ParamField body="output_tokens" type="number">
  Completion (output) tokens used by the generation. Assistant events.
</ParamField>

<ParamField body="reasoning_tokens" type="number">
  Tokens used for extended thinking or reasoning output.
</ParamField>

<ParamField body="cache_read_tokens" type="number">
  Prompt tokens served from the provider's cache.
</ParamField>

<ParamField body="cache_creation_tokens" type="number">
  Prompt tokens written to the provider's cache.
</ParamField>

<ParamField body="model" type="string">
  Model ID, for example `gpt-4o` or `claude-sonnet-4-20250514`.
</ParamField>

<ParamField body="provider" type="string">
  Provider name, for example `openai` or `anthropic`.
</ParamField>

<ParamField body="finish_reason" type="string">
  Why the model stopped generating, for example `stop`, `length`, or `tool_calls`. Assistant events.
</ParamField>

<ParamField body="prompt_id" type="string">
  Moda prompt ID, linking the event to a managed prompt. See [Prompt attribution](/prompt-management/attribution).
</ParamField>

<ParamField body="prompt_name" type="string">
  Name of the managed prompt used for this generation.
</ParamField>

<ParamField body="prompt_version" type="string">
  Version of the managed prompt used for this generation.
</ParamField>

<ParamField body="prompt_version_id" type="string">
  Moda prompt version ID, from `.moda/prompts.lock.json`.
</ParamField>

<ParamField body="agent_name" type="string">
  In multi-agent systems, the agent that authored this event.
</ParamField>

<ParamField body="external_message_id" type="string">
  Your own stable, unique ID for this message.
</ParamField>

<ParamField body="environment" type="string">
  Per-event override of the request-level environment. Common aliases are normalized (`dev`/`develop` → `development`, `stg`/`stage` → `staging`, `prod` → `production`); unrecognized values fall back to `production`.
</ParamField>

<Note>
  Events carrying a `messageType` field are rejected with `400`. Channel-specific events (chat platforms, email, calls, tool invocations) belong on `POST /v1/ingest/multi`.
</Note>

### Content blocks

Use content blocks (in `content` or `content_blocks`) when a message contains more than plain text:

| Block `type`  | Fields                                        | Description                               |
| ------------- | --------------------------------------------- | ----------------------------------------- |
| `text`        | `text`                                        | Plain text                                |
| `thinking`    | `text`                                        | Model reasoning (extended thinking)       |
| `tool_use`    | `tool_name`, `tool_use_id`, `input`           | Tool or function call issued by the model |
| `tool_result` | `tool_use_id`, `content`, `is_error`          | Result returned by a tool                 |
| `image`       | `source` (`{type, media_type?, data?, url?}`) | Image content (base64 or URL)             |

Every block accepts an optional numeric `index` (defaults to the block's position in the array). Anthropic API field names are also accepted: `name` is normalized to `tool_name`, `id` to `tool_use_id`, and a `thinking` field on thinking blocks to `text`.

```json theme={"dark"}
{
  "conversation_id": "conv-abc123",
  "role": "assistant",
  "content_blocks": [
    {"type": "text", "text": "Let me look that up."},
    {"type": "tool_use", "tool_name": "web_search", "tool_use_id": "toolu_01", "input": {"query": "refund policy"}}
  ]
}
```

### Example

```bash theme={"dark"}
curl https://moda-ingest.modas.workers.dev/v1/ingest \
  -H "Authorization: Bearer YOUR_MODA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "environment": "production",
    "events": [
      {
        "conversation_id": "conv-abc123",
        "role": "user",
        "message": "What is the capital of France?",
        "timestamp": "2026-08-16T10:30:00Z",
        "user_id": "user-789",
        "external_message_id": "msg-001"
      },
      {
        "conversation_id": "conv-abc123",
        "role": "assistant",
        "message": "The capital of France is Paris.",
        "timestamp": "2026-08-16T10:30:01Z",
        "model": "gpt-4o",
        "provider": "openai",
        "input_tokens": 12,
        "output_tokens": 8,
        "cache_read_tokens": 0,
        "finish_reason": "stop",
        "agent_name": "support-bot",
        "external_message_id": "msg-002"
      }
    ]
  }'
```

Success response:

```json theme={"dark"}
{
  "success": true,
  "count": 2,
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
```

Validation errors return `400` with a message naming the first failing event; nothing from the batch is stored:

```json theme={"dark"}
{
  "success": false,
  "count": 0,
  "message": "Event 0: missing or invalid 'conversation_id'",
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
```

Temporary ingestion outages return `503` with `retryable: true`:

```json theme={"dark"}
{
  "success": false,
  "count": 0,
  "message": "Queue temporarily unavailable",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "retryable": true
}
```

The full status-code table and retry guidance are on the [Reliability](/ingestion/reliability) page.

## POST /v1/ingest/multi

Ingests conversations that span channels: chat platform messages, tool invocations, emails, and call transcripts. Each event declares its shape with `messageType`.

### Common event fields

<ParamField body="id" type="string" required>
  Your stable, unique ID for the event (non-empty). It becomes the event's `external_message_id`.
</ParamField>

<ParamField body="messageType" type="string" required>
  One of `channel`, `tool_call`, `email`, or `call`.
</ParamField>

<ParamField body="conversationId" type="string" required>
  Conversation or thread ID that groups events, required for all four event types.
</ParamField>

<ParamField body="userId" type="string">
  Identifier for the end user.
</ParamField>

<ParamField body="timestamp" type="string">
  ISO 8601 date-time. Defaults to the time the event is received.
</ParamField>

<ParamField body="metadata" type="object">
  Free-form object. Accepted, but its contents are not currently attached to the stored message.
</ParamField>

The request body has the same envelope as `/v1/ingest`: an optional request-level `environment` plus an `events` array of up to 1,000 events.

### Type-specific fields

**`channel`** — a message in a chat or messaging platform:

| Field     | Type   | Required | Description                              |
| --------- | ------ | -------- | ---------------------------------------- |
| `message` | string | Yes      | Message content (may be an empty string) |
| `role`    | string | Yes      | `user`, `assistant`, or `system`         |

**`tool_call`** — a tool or function invocation:

| Field              | Type   | Required | Description                                   |
| ------------------ | ------ | -------- | --------------------------------------------- |
| `name`             | string | Yes      | Tool or function name                         |
| `toolCallRequest`  | any    | No       | Request payload (any JSON, including `null`)  |
| `toolCallResponse` | any    | No       | Response payload (any JSON, including `null`) |

**`email`** — one email in a thread:

| Field       | Type             | Required | Description                                |
| ----------- | ---------------- | -------- | ------------------------------------------ |
| `role`      | string           | Yes      | `user` (inbound) or `assistant` (outbound) |
| `body`      | string           | Yes      | Email body                                 |
| `subject`   | string           | No       | Subject line                               |
| `inReplyTo` | string           | No       | Message-ID of the parent email             |
| `from`      | string           | No       | Sender address                             |
| `to`        | array of strings | No       | Recipient addresses                        |

**`call`** — a phone, video, or voice call. Each call is stored as a single record containing the full transcript:

| Field        | Type   | Required | Description                                                                                                     |
| ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------------- |
| `transcript` | array  | Yes      | Non-empty array of `{role, content, timestamp?}` speaker turns; `role` and `content` are required on every turn |
| `duration`   | number | No       | Call duration in seconds                                                                                        |
| `callType`   | string | No       | `phone`, `video`, or `voice`                                                                                    |

### Example

```bash theme={"dark"}
curl https://moda-ingest.modas.workers.dev/v1/ingest/multi \
  -H "Authorization: Bearer YOUR_MODA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "messageType": "channel",
        "id": "msg-001",
        "conversationId": "support-thread-456",
        "message": "I need help with my subscription",
        "role": "user",
        "userId": "customer-789"
      },
      {
        "messageType": "tool_call",
        "id": "tool-001",
        "conversationId": "support-thread-456",
        "name": "get_subscription",
        "toolCallRequest": {"user_id": "customer-789"},
        "toolCallResponse": {"plan": "pro", "status": "active"}
      }
    ]
  }'
```

Success response — `details` breaks the accepted count down by event type:

```json theme={"dark"}
{
  "success": true,
  "count": 2,
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "details": {
    "channel": 1,
    "tool_call": 1,
    "email": 0,
    "call": 0,
    "call_transcript_messages": 0
  }
}
```

<Note>
  `details.call_transcript_messages` is always `0`; it is retained for backward compatibility. Calls are counted under `details.call`.
</Note>

Validation failures return `400` with a per-event message (for example `Event 0: Missing or invalid 'id' field`), and nothing from the batch is stored.

## POST /v1/traces

The OpenTelemetry OTLP/HTTP trace endpoint. Both Moda SDKs export here by default, and any OTLP-capable exporter can target it directly.

* Accepts OTLP/JSON, and OTLP/protobuf when the `Content-Type` header contains `protobuf`.
* The body is a standard `ExportTraceServiceRequest` (`resourceSpans` → `scopeSpans` → `spans`).
* Conversations are extracted from GenAI span attributes: OTel GenAI semantic conventions (`gen_ai.*`), OpenLLMetry (`llm.prompts.*` / `llm.completions.*`), Vercel AI SDK (`ai.*`), and `moda.*` attributes for conversation ID, user ID, environment, and prompt attribution.
* Only the 5 MB body cap applies; there is no per-request span count limit.

Responses: JSON requests get the standard envelope (`200` with `{"success": true, "count": N, "requestId": "..."}`, `400` with the message `Invalid OTLP format` on parse failure). Successful protobuf requests get an OTLP `ExportTraceServiceResponse` body instead; parse and auth failures return the JSON envelope regardless of request format.

See [OpenTelemetry](/ingestion/opentelemetry) for the full list of supported span attributes and the conversation ID precedence rules.

<Note>
  Coding-agent telemetry (Claude Code, Codex, Cursor) uses dedicated `/v1/otel/...` routes documented on the [coding agents](/ingestion/coding-agents) page.
</Note>

## Next steps

* [Reliability](/ingestion/reliability) — status codes, retry guidance, and limits.
* [OpenTelemetry](/ingestion/opentelemetry) — supported span attributes for `POST /v1/traces`.
* [Prompt attribution](/prompt-management/attribution) — how the `prompt_*` fields link events to prompt versions.
* [Authentication](/administration/authentication) — creating and revoking ingestion keys.
