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:
-H "Authorization: Bearer YOUR_MODA_API_KEY"
You can set the environment at the request level to apply to all events:
{
"environment": "staging",
"events": [...]
}
Send an array of events in the request body:
{
"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) |
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.
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:
{
"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:
{
"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."}
]
}]
}
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.
{
"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 and function invocations made by the LLM during a conversation, with full request and response data.
{
"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.
{
"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 |
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).
Call transcripts
LLM-powered conversations happening over phone or video calls, with full speaker turn transcripts.
{
"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 |
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.
Mixed batch example
You can mix standard events and channel-specific events in the same request:
{
"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"}
}
]
}
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.
Example
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"
}
]
}'
Response
Success response
{
"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:
{
"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
{
"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 |
For 503 errors, use exponential backoff when retrying. Start with 1 second and double each retry, up to a maximum of 30 seconds.