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

# Direct API

> Send LLM conversation data directly to the Moda API across any channel

## Overview

Send data directly to the Moda ingest API. This supports LLM conversations across any channel:

* Standard chat completions (OpenAI, Anthropic, etc.)
* Chat/messaging platforms (Slack, Discord, etc.)
* Email conversations
* Voice and video call transcripts
* Tool/function calls within conversations
* Custom integrations and languages without Moda SDK support

All events are processed by Moda and appear in your dashboard alongside SDK-ingested data.

## Endpoint

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

## Authentication

Include your Moda API key in the `Authorization` header:

```bash theme={"dark"}
-H "Authorization: Bearer YOUR_MODA_API_KEY"
```

## Request format

You can set the `environment` at the request level to apply to all events:

```json theme={"dark"}
{
  "environment": "staging",
  "events": [...]
}
```

Send an array of events in the request body:

```json theme={"dark"}
{
  "events": [
    {
      "conversation_id": "conv-123",
      "role": "user",
      "message": "What is the capital of France?",
      "timestamp": "2024-01-15T10:30:00Z"
    },
    {
      "conversation_id": "conv-123",
      "role": "assistant",
      "message": "The capital of France is Paris.",
      "timestamp": "2024-01-15T10:30:01Z",
      "input_tokens": 12,
      "output_tokens": 8,
      "model": "gpt-4o",
      "provider": "openai"
    }
  ]
}
```

## Event fields

### Required fields

| Field                  | Type                            | Description                                                                                              |
| ---------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `conversation_id`      | string                          | Unique ID for the conversation                                                                           |
| `role`                 | string                          | One of: `user`, `assistant`, `system`                                                                    |
| `message` or `content` | string (or array for `content`) | The message content. Provide either `message` (string) or `content` (string or array of content blocks). |

### Optional fields

| Field              | Type   | Description                                                                                                                   |
| ------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `timestamp`        | string | ISO 8601 timestamp (defaults to now)                                                                                          |
| `trace_id`         | string | For linking related events (defaults to conversation\_id)                                                                     |
| `user_id`          | string | Identifier for the end user                                                                                                   |
| `input_tokens`     | number | Number of input/prompt tokens used                                                                                            |
| `output_tokens`    | number | Number of output/completion tokens used                                                                                       |
| `reasoning_tokens` | number | Tokens used for extended thinking (Claude models)                                                                             |
| `model`            | string | Model name (e.g., `gpt-4o`, `claude-3-opus`)                                                                                  |
| `provider`         | string | Provider name (e.g., `openai`, `anthropic`)                                                                                   |
| `environment`      | string | Environment name: `development`, `staging`, or `production` (defaults to `production`). Can also be set at the request level. |
| `prompt_id`        | string | Prompt template ID (for prompt management tracking)                                                                           |
| `prompt_name`      | string | Prompt template name                                                                                                          |
| `prompt_version`   | string | Prompt template version                                                                                                       |
| `content_blocks`   | array  | Structured content blocks (see below)                                                                                         |

<Tip>
  When using `content` as an array of content blocks, text is automatically extracted from `text`-type blocks for search and analytics. If both `message` and `content` are provided, `message` takes precedence.
</Tip>

### Content blocks

For conversations with tool use, extended thinking, or images, include structured content blocks:

| Block Type    | Fields                               | Description                         |
| ------------- | ------------------------------------ | ----------------------------------- |
| `text`        | `text`                               | Plain text content                  |
| `thinking`    | `text`                               | Model reasoning (extended thinking) |
| `tool_use`    | `tool_name`, `tool_use_id`, `input`  | Tool/function call                  |
| `tool_result` | `tool_use_id`, `content`, `is_error` | Tool response                       |
| `image`       | `source`                             | Image (base64 or URL)               |

Example with tool use:

```json theme={"dark"}
{
  "events": [{
    "conversation_id": "conv-123",
    "role": "assistant",
    "message": "Let me search for that.",
    "content_blocks": [
      {"type": "text", "text": "Let me search for that."},
      {"type": "tool_use", "tool_name": "web_search", "tool_use_id": "toolu_abc", "input": {"query": "latest news"}}
    ]
  }]
}
```

Example with extended thinking:

```json theme={"dark"}
{
  "events": [{
    "conversation_id": "conv-123",
    "role": "assistant",
    "message": "The answer is 42.",
    "reasoning_tokens": 150,
    "output_tokens": 10,
    "content_blocks": [
      {"type": "thinking", "text": "Let me think through this step by step..."},
      {"type": "text", "text": "The answer is 42."}
    ]
  }]
}
```

## Channel-specific event formats

LLM conversations happen across many channels beyond standard chat completions. Use the `messageType` field to send conversations from messaging platforms, email, voice calls, and to log tool invocations. You can mix these with standard events in the same request.

### Channel messages

LLM-powered conversations in chat platforms or messaging systems.

```json theme={"dark"}
{
  "messageType": "channel",
  "id": "msg-123",
  "conversationId": "thread-456",
  "message": "Hello, how can I help you today?",
  "role": "assistant",
  "userId": "user-789",
  "timestamp": "2025-01-04T12:00:00Z"
}
```

| Field            | Type   | Required | Description                             |
| ---------------- | ------ | -------- | --------------------------------------- |
| `messageType`    | string | Yes      | Must be `"channel"`                     |
| `id`             | string | Yes      | Unique message ID                       |
| `conversationId` | string | Yes      | Thread/channel ID for grouping messages |
| `message`        | string | Yes      | Message content                         |
| `role`           | string | Yes      | One of: `user`, `assistant`, `system`   |
| `userId`         | string | No       | User identifier                         |
| `timestamp`      | string | No       | ISO 8601 timestamp (defaults to now)    |
| `metadata`       | object | No       | Custom metadata                         |

### Tool calls

Tool and function invocations made by the LLM during a conversation, with full request and response data.

```json theme={"dark"}
{
  "messageType": "tool_call",
  "id": "tool-123",
  "conversationId": "thread-456",
  "name": "search_knowledge_base",
  "toolCallRequest": {
    "query": "refund policy",
    "limit": 5
  },
  "toolCallResponse": {
    "results": [
      {"title": "Refund Policy", "content": "..."}
    ]
  }
}
```

| Field              | Type   | Required | Description                        |
| ------------------ | ------ | -------- | ---------------------------------- |
| `messageType`      | string | Yes      | Must be `"tool_call"`              |
| `id`               | string | Yes      | Unique tool call ID                |
| `conversationId`   | string | Yes      | Conversation ID for grouping       |
| `name`             | string | Yes      | Tool/function name                 |
| `toolCallRequest`  | any    | No       | Request payload (can be any JSON)  |
| `toolCallResponse` | any    | No       | Response payload (can be any JSON) |
| `userId`           | string | No       | User identifier                    |
| `timestamp`        | string | No       | ISO 8601 timestamp                 |
| `metadata`         | object | No       | Custom metadata                    |

### Emails

LLM-powered conversations happening over email, with threading support via `conversationId` and optional `inReplyTo` for referencing specific parent emails.

```json theme={"dark"}
{
  "messageType": "email",
  "id": "email-123",
  "conversationId": "conv-456",
  "role": "user",
  "subject": "Re: Order #12345 - Shipping question",
  "body": "Thank you for reaching out. Your order is scheduled to arrive...",
  "inReplyTo": "parent-email-message-id",
  "from": "support@example.com",
  "to": ["customer@example.com"]
}
```

| Field            | Type   | Required | Description                                           |
| ---------------- | ------ | -------- | ----------------------------------------------------- |
| `messageType`    | string | Yes      | Must be `"email"`                                     |
| `id`             | string | Yes      | Unique email ID                                       |
| `conversationId` | string | Yes      | Thread/conversation ID (groups emails together)       |
| `role`           | string | Yes      | `"user"` (inbound) or `"assistant"` (outbound)        |
| `body`           | string | Yes      | Email body content                                    |
| `inReplyTo`      | string | No       | Email thread reference (e.g. parent email Message-ID) |
| `subject`        | string | No       | Email subject line                                    |
| `from`           | string | No       | Sender email address                                  |
| `to`             | array  | No       | Recipient email addresses                             |
| `userId`         | string | No       | User identifier                                       |
| `timestamp`      | string | No       | ISO 8601 timestamp                                    |

<Note>
  The `conversationId` field groups emails into conversations, consistent with all other event types.
  Use `inReplyTo` optionally to reference specific parent emails within a conversation (e.g. when there are multiple email threads within the same conversation).
</Note>

### Call transcripts

LLM-powered conversations happening over phone or video calls, with full speaker turn transcripts.

```json theme={"dark"}
{
  "messageType": "call",
  "id": "call-123",
  "conversationId": "call-session-456",
  "transcript": [
    {"role": "user", "content": "Hi, I'm having trouble with my account."},
    {"role": "assistant", "content": "I'd be happy to help. Can you tell me more about the issue?"},
    {"role": "user", "content": "I can't log in after resetting my password."}
  ],
  "duration": 180,
  "callType": "phone"
}
```

| Field            | Type   | Required | Description                             |
| ---------------- | ------ | -------- | --------------------------------------- |
| `messageType`    | string | Yes      | Must be `"call"`                        |
| `id`             | string | Yes      | Unique call ID                          |
| `conversationId` | string | Yes      | Call session ID                         |
| `transcript`     | array  | Yes      | Array of transcript entries (see below) |
| `duration`       | number | No       | Call duration in seconds                |
| `callType`       | string | No       | One of: `phone`, `video`, `voice`       |
| `userId`         | string | No       | User identifier                         |
| `timestamp`      | string | No       | ISO 8601 timestamp                      |
| `metadata`       | object | No       | Custom metadata                         |

**Transcript entry format:**

| Field       | Type   | Required | Description                                                   |
| ----------- | ------ | -------- | ------------------------------------------------------------- |
| `role`      | string | Yes      | Speaker role (e.g., `user`, `assistant`, `customer`, `agent`) |
| `content`   | string | Yes      | What was said                                                 |
| `timestamp` | string | No       | When this was said                                            |

<Note>
  Call transcripts are stored as a single record containing the full transcript.
  The complete transcript is searchable in the Moda dashboard,
  and the structured speaker turns are preserved for detailed analysis.
</Note>

### Mixed batch example

You can mix standard events and channel-specific events in the same request:

```json theme={"dark"}
{
  "events": [
    {
      "conversation_id": "conv-123",
      "role": "user",
      "message": "What is the weather?"
    },
    {
      "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"}
    }
  ]
}
```

<Tip>
  Each event is validated individually. Standard events (without `messageType`) use the fields documented above, while channel-specific events are validated according to their `messageType` schema.
</Tip>

## Example

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl https://moda-ingest.modas.workers.dev/v1/ingest \
    -H "Authorization: Bearer YOUR_MODA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "events": [
        {
          "conversation_id": "conv-abc123",
          "role": "user",
          "message": "Hello, how are you?"
        },
        {
          "conversation_id": "conv-abc123",
          "role": "assistant",
          "message": "I am doing well, thank you for asking!",
          "input_tokens": 8,
          "output_tokens": 12,
          "model": "gpt-4o",
          "provider": "openai"
        }
      ]
    }'
  ```

  ```python Python theme={"dark"}
  import requests

  response = requests.post(
      "https://moda-ingest.modas.workers.dev/v1/ingest",
      headers={
          "Authorization": "Bearer YOUR_MODA_API_KEY",
          "Content-Type": "application/json"
      },
      json={
          "events": [
              {
                  "conversation_id": "conv-abc123",
                  "role": "user",
                  "message": "Hello, how are you?"
              },
              {
                  "conversation_id": "conv-abc123",
                  "role": "assistant",
                  "message": "I am doing well, thank you for asking!",
                  "input_tokens": 8,
                  "output_tokens": 12,
                  "model": "gpt-4o",
                  "provider": "openai"
              }
          ]
      }
  )

  print(response.json())
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch('https://moda-ingest.modas.workers.dev/v1/ingest', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_MODA_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      events: [
        {
          conversation_id: 'conv-abc123',
          role: 'user',
          message: 'Hello, how are you?'
        },
        {
          conversation_id: 'conv-abc123',
          role: 'assistant',
          message: 'I am doing well, thank you for asking!',
          input_tokens: 8,
          output_tokens: 12,
          model: 'gpt-4o',
          provider: 'openai'
        }
      ]
    })
  });

  console.log(await response.json());
  ```
</CodeGroup>

## Response

### Success response

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

When the request includes channel-specific events (with `messageType`), the response includes a `details` breakdown:

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

### Error response

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

| Field       | Type    | Description                                                                              |
| ----------- | ------- | ---------------------------------------------------------------------------------------- |
| `success`   | boolean | Whether the request succeeded                                                            |
| `count`     | number  | Number of events processed                                                               |
| `requestId` | string  | Unique request ID for debugging                                                          |
| `message`   | string  | Error message (on failure)                                                               |
| `retryable` | boolean | Whether the error is temporary and should be retried                                     |
| `details`   | object  | Breakdown by channel event type (only present when channel-specific events are included) |

## Batch limits

| Limit                  | Value  |
| ---------------------- | ------ |
| Max events per request | 1,000  |
| Max message size       | 100 KB |
| Max request size       | 5 MB   |

## Error handling

| Status | Meaning                         | Retryable |
| ------ | ------------------------------- | --------- |
| 200    | Success                         | -         |
| 400    | Invalid request format          | No        |
| 401    | Invalid or missing API key      | No        |
| 413    | Request too large               | No        |
| 503    | Service temporarily unavailable | Yes       |

<Tip>
  For 503 errors, use exponential backoff when retrying. Start with 1 second and double each retry, up to a maximum of 30 seconds.
</Tip>
