Sending conversations
Send one request per finished conversation to POST /v1/conversations. Send it from wherever your conversations end: your voice platform’s end-of-call webhook, or your chat backend when a session closes.
The payload
Section titled “The payload”| Field | Required | |
|---|---|---|
external_id |
yes | Your ID for this call or chat, 1 to 200 characters. Unique per environment. |
channel |
yes | voice or chat |
transcript |
yes | Every turn, in order. At least one. |
agent |
{ id?, name?, version? }: the AI agent that handled it. See Agents. |
|
group |
{ id, name? }: files a new agent into a group. |
|
events |
What your agent did: tool calls, transfers, handoffs. | |
started_at, ended_at |
ISO 8601 with an offset, e.g. 2026-10-05T14:02:11Z. ended_at defaults to when Midwater received it, and decides which day the conversation counts toward. |
|
ended_by |
agent, caller, user, human_agent, timeout (chat only) or system |
|
metadata |
Any JSON object, kept with the conversation and returned as sent. |
Transcript turns
Section titled “Transcript turns”Each turn is { speaker, text, start_ms?, end_ms?, asr_confidence? }:
speaker:agent(your AI agent),user(the caller or chat user) orhuman_agent(a person who took over).text: what was said, at least one character.start_ms,end_ms: offsets from the start of the conversation, for voice.asr_confidence: your speech recognition’s confidence for the turn, 0 to 1. Turns below 0.8 are marked as possibly misheard when Midwater’s model reads the conversation.
{ "external_id": "call_8f2a91", "channel": "voice", "started_at": "2026-10-05T14:02:11Z", "ended_at": "2026-10-05T14:06:40Z", "ended_by": "caller", "agent": { "id": "front-desk", "name": "Front desk", "version": "3.2.0" }, "transcript": [ { "speaker": "agent", "text": "Thanks for calling, this call may be recorded. How can I help?", "start_ms": 0, "end_ms": 2400 }, { "speaker": "user", "text": "I need to move my appointment to Thursday.", "start_ms": 2900, "end_ms": 5100, "asr_confidence": 0.93 }, { "speaker": "agent", "text": "Sure, I have Thursday at 10 AM. Does that work?", "start_ms": 5600, "end_ms": 8200 }, { "speaker": "user", "text": "Yes, that's perfect.", "start_ms": 8700, "end_ms": 9900, "asr_confidence": 0.96 }, { "speaker": "agent", "text": "Great, you're all set for Thursday at 10 AM.", "start_ms": 61500, "end_ms": 64000 } ], "events": [ { "type": "tool_call", "name": "reschedule_appointment", "status": "success", "at_ms": 61000 } ], "metadata": { "language": "en", "phone_line": "main" }}{ "external_id": "chat_51c0", "channel": "chat", "ended_by": "timeout", "agent": { "id": "web-chat", "name": "Website chat", "version": "2026.10.1" }, "transcript": [ { "speaker": "agent", "text": "Hi! How can I help today?" }, { "speaker": "user", "text": "Can I get a refund on order 1042?" }, { "speaker": "agent", "text": "I can't do refunds here. Let me get someone from our team." }, { "speaker": "human_agent", "text": "Hi, this is Sam. I've started the refund for order 1042." } ], "events": [ { "type": "handoff", "target": "support-team", "at_ms": 41000 } ]}Agents and groups
Section titled “Agents and groups”Midwater measures each AI agent separately, so tell it which agent handled each conversation:
agent.idis your stable ID for the agent, such asfront-desk. IDs use letters, digits,.,_,:and-, start with a letter or digit, and are at most 128 characters. They never need URL-encoding.- A new
agent.idcreates the agent the first time it’s seen, namedagent.nameif you send one. - Without
agent.id, the conversation goes to the project’s Default agent (IDdefault). agent.versionis your agent’s release, such as3.2.0. New releases are marked on the agent’s trend in the app, and health reasons can name them (“since version 3.2.0”).
group: { id, name? } files the agent into a group, creating the group if needed, the first time the agent is seen without one. It never moves an agent that’s already in a group, and once someone places the agent in the app (including moving it to No group), group is ignored for that agent; the conversation is still accepted. Groups are how agencies keep each client’s agents together.
Events
Section titled “Events”events tells Midwater what your agent did, so checks can rely on facts instead of reading between the lines. Each event has a type and optional name, status, target and at_ms; extra properties are kept.
| Event | Effect |
|---|---|
{ "type": "tool_call", "name": "book_appointment", "status": "success" } |
A tool call whose name contains book or schedul turns on Midwater’s built-in booking checks for the project the first time one arrives. They compare what the agent told the caller with whether a booking tool call really succeeded. A built-in check someone turned off stays off. |
{ "type": "transfer", "target": "front-desk-line" } or { "type": "handoff" } |
Marks the conversation as handed to a person (outcome escalated). |
The response
Section titled “The response”| Status | Body | Meaning |
|---|---|---|
202 |
{ "id", "status": "queued" } |
Stored and queued for scoring. |
200 |
{ "id", "status", "duplicate": true } |
A conversation with this external_id already exists in this environment. Nothing new was stored. |
400 |
invalid_json |
The body isn’t JSON. |
422 |
validation_error with fields |
The body doesn’t match the schema. See Errors. |
Scoring runs in the background and usually finishes within seconds. Read the result with GET /v1/conversations/{id} or receive it with the conversation.evaluated webhook.
Retries and idempotency
Section titled “Retries and idempotency”Sending the same conversation twice never stores it twice:
external_idis unique per environment. Re-sending one answers200with the existing conversation and"duplicate": true.Idempotency-Keymakes a retried request return exactly the first response, with the headerIdempotent-Replayed: true. Use any string unique to the request, such as the conversation’sexternal_id. Keys are scoped to the environment. Only stored conversations (200and202) are remembered, so a request that failed validation can be corrected and retried with the same key.
Planned Idempotency keys will apply to every POST and expire after 24 hours, and reusing a key with a different body will answer 409 with idempotency_conflict. Until then, keys don’t expire and bodies aren’t compared, so never reuse a key for a different conversation.
Retry network errors, 408, 429 and 5xx with backoff, always with the same idempotency key. Don’t retry other 4xx. The SDKs do this for you and send an idempotency key on every POST.