aethexDocs

API Reference

Conversation API

Real-time voice sessions between a user (in a browser or mobile app) and one of your agents. WebRTC keeps the latency in the same range as a phone call, no phone number involved.The handshake, in plain terms:
1. Your server opens a session for an agent with POST /conversation/connect. You get a session_id and the network config (ice_config) the browser needs.
2. The browser builds an SDP offer with RTCPeerConnection and posts it through your server, which forwards it to POST /conversation/:session_id/offer and returns the SDP answer. The browser sets it on the peer connection and audio starts flowing.
3. Return tool results with /tool-result when the agent invokes a tool; end the call with /end.Your own audio client: to stream the conversation over a WebSocket instead, for example from a server, a device or a phone system you run, open the session with transport: "websocket". You get a ws_url in place of the ice_config, and your client streams audio over it (WS /ws/v1/conversation/:session_id below). The flow lives in the WebSocket calls concept.Most endpoints use calls:write; /status accepts calls:read. The high-level flow lives in the WebRTC calls concept.

Base URL
https://api.aethexai.com/api/v1
Endpoints
8
Scopes
calls:readcalls:write
POST/conversation/connect

Open a session for an agent. Server-side; uses your API key directly.

Connect

Starts a WebRTC session for the given agent. You get back a session_id and an ice_config. Pass the ice_config.iceServers directly into new RTCPeerConnection({ iceServers }) on the browser side.The session_id is the same UUID as the underlying conversation and call records, so you can use it later for GET /conversations/{id}/transcript and GET /recordings/{id}/audio once the call ends.Each call creates persistent records; do not invoke in a tight retry loop. Public agents: if the agent has public_access on, a browser or app can call this endpoint with only the agent_id and no API key. Restrict which sites may embed it with the agent's allowed_origins.WebSocket sessions: with transport: "websocket", the response is a session_id and a ws_url instead, for your client to stream the conversation over (see WS /ws/v1/conversation/:session_id below). Only an API key can open one. By default the agent greets the user first; set direction to outbound when your side placed the call.

Request body

7
  • agent_idstring (UUID)required

    The agent UUID to start the session with. Must belong to the calling account.

  • dynamic_variablesobjectoptional

    Values for the agent's {{variable}} placeholders, for this session only — nothing is saved back to the agent. Same shape as POST /calls/trigger: a flat object of strings, numbers or booleans. Values you send override the agent's stored defaults; a placeholder with neither is never read out as braces: the model sees [name: not provided] in its place, and the spoken opener drops it. Keys may not start with system__ — those are filled in for you, and system__caller_id / system__called_number are empty on a browser call because there is no phone leg. On a WebSocket session, caller_phone fills the one for its side of the call. Unlike POST /calls/trigger, an unsupported system__ placeholder is treated the same way rather than rejected. Ignored when the session itself is keyless — a public agent reached without an API key, or a call token — because those credentials live in the browser; those sessions use the agent's own defaults. Reaching that same public agent with your API key personalizes normally: what decides is the credential, not the agent.

  • transportstringoptional

    webrtc (the default) for a browser or app peer, or websocket to stream the conversation's audio over a WebSocket from your own code. A websocket session needs an API key and returns a ws_url instead of an ice_config.

  • directionstringoptional

    WebSocket sessions only. inbound (the default): the agent greets the user as soon as the socket connects. outbound: your side placed the call, so the agent lets the person who answered speak first, and greets them after their hello or after 3 seconds of silence. Ignored on a WebRTC session.

  • protocolstringoptional

    WebSocket sessions only. aethex (the default): Aethex's JSON events, with what the user and the agent said as text. twilio: the Twilio Media Streams format, for systems that already speak it. Ignored on a WebRTC session.

  • audio_formatstringoptional

    WebSocket sessions only. The audio on the wire, both ways: pcm_16000 (16-bit PCM at 16 kHz, the default) or ulaw_8000 (G.711 µ-law at 8 kHz). The twilio protocol takes ulaw_8000 only. Ignored on a WebRTC session.

  • caller_phonestring (E.164)optional

    WebSocket sessions only. The phone number of the person on the call, in E.164 format (for example +14155550199), when the call has one. Saved as the call's from_number on an inbound session and its to_number on an outbound one, and fills system__caller_id or system__called_number to match. Ignored on a WebRTC session.

Errors

8
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

API key lacks the calls:write scope.

Use a key with the required scope.

403Forbidden

transport is websocket, but the request did not use an API key: for example a call token, or a public agent reached without one.

Open WebSocket sessions from your server with an API key.

402Payment Required

Account is hard-blocked: subscription canceled past the grace window, or balance is zero with no payment method on file.

Re-subscribe, add a payment method, or top up credits, then retry.

404Agent not found

The agent_id does not exist or does not belong to this account.

Verify via GET /api/v1/agents.

422Invalid request body

agent_id missing or not a valid UUID, or invalid dynamic_variables — not a flat object of strings, numbers and booleans, or a key using the reserved system__ prefix. Also a direction, protocol or audio_format outside its values, audio_format pcm_16000 with protocol twilio, or, on a WebSocket session, a caller_phone that is not in E.164 format.

Send a JSON body with a valid agent_id. If you send dynamic_variables, keep it a flat object of scalar values and drop any system__ key. Send caller_phone in E.164 format, such as +14155550199.

429At capacity

The account or shared backend has no free slot. Response body includes retry_after; response carries a Retry-After header.

Honor the Retry-After header and retry.

503No capacity available

No backend slot can be reserved at all.

Retry shortly. If persistent, contact our team at [email protected].

POST/conversation/token

Short-lived, single-agent token for direct browser signaling — no proxy.

Mint a call token

Mint an ephemeral token (a signed JWT) scoped to a single agent, so a browser can call /connect, /offer, /ice, /status, and /end directly with an Authorization: Bearer <token> header — instead of routing every signaling request through a proxy. Call this server-side with your API key; never ship the key to the browser. The token lasts long enough to cover the whole call (it defaults to your agent's maximum call duration plus a short margin, clamped to 60–4,320 seconds), only works for the agent it was minted for, and carries only the calls scopes of the minting key. The browser can call from any origin: the token (a bearer credential, not a cookie) is what authenticates the request, so there's no CORS allow-list to configure.

Request body

2
  • agent_idstring (UUID)required

    The agent UUID the token authorizes. Must belong to the calling account.

  • ttl_secondsintegeroptional

    Token lifetime in seconds. Optional; defaults to the agent's maximum call duration plus a short margin, clamped to 60–4,320.

Errors

5
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

API key lacks the calls:write scope.

Use a key with the required scope.

404Agent not found

The agent_id does not exist or does not belong to this account.

Verify via GET /api/v1/agents.

422Invalid request body

agent_id missing or not a valid UUID, or ttl_seconds not a whole number.

Send a JSON body with a valid agent_id. Omit ttl_seconds to get the agent's own call duration plus a margin, or send whole seconds — a value outside 60–4,320 is clamped into range, not refused.

429Rate limited

Minting a token is an ordinary API-key POST, so it counts against your key's rate limit. Too many mints in a short window return 429.

Mint one token per call and reuse it for that call's signaling; honor the Retry-After header before retrying.

POST/conversation/:session_id/offer

Send your client SDP offer; receive the server SDP answer plus a pc_id for follow-up requests.

Submit SDP Offer

Submits an SDP offer for the session created by /connect. The response carries the SDP answer and a pc_id you reuse on renegotiation and ICE trickle. Pass restart_pc: true to force a fresh peer connection (used to recover from a stuck ICE state). ICE candidates can either be bundled into the offer SDP or trickled later via PATCH /conversation/{session_id}/ice.

Path & Query

1
  • session_idstring (UUID)required

    The session ID returned by /connect.

Request body

4
  • sdpstringrequired

    The SDP offer string generated by the client RTCPeerConnection.

  • typestringrequired

    SDP type. "offer" on initial requests; "answer" is accepted on the symmetric server-offer flow.

  • pc_idstringoptional

    PeerConnection identifier returned by the first offer response. Omit on the very first offer; required on renegotiation or ICE restart.

  • restart_pcbooleanoptional

    If true, force the server to tear down and recreate the peer connection. Used to recover from a stuck ICE state.

Errors

8
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

Session is owned by a different account.

Use a key on the owning account.

404Session not found or expired

No record for this session_id. The session was never created, has expired, or was already ended.

Call /connect again to mint a new session and retry the offer.

409Not a WebRTC session

The session was opened with transport: "websocket". Its audio flows over the ws_url, so it takes no SDP offer.

Connect your client to the session's ws_url, or open a WebRTC session for a browser peer.

422Invalid SDP body

Body is missing sdp or type, or fails validation.

Generate the offer via RTCPeerConnection.createOffer() and pass the resulting { sdp, type } verbatim.

429At capacity

Account or backend is at capacity and the session could not be re-leased. Response includes retry_after.

Honor the Retry-After header and retry.

500Failed to start session

The peer connection was created but the agent pipeline failed to start. Capacity is released automatically.

Retry the offer. If persistent, contact our team at [email protected].

503No capacity available

No backend slot can be reserved.

Retry shortly.

PATCH/conversation/:session_id/ice

Stream ICE candidates as they are discovered. Exempt from per-account rate limits.

Trickle ICE Candidates

Streams ICE candidates discovered by the client RTCPeerConnection after the initial SDP exchange. Pass the pc_id from the /offer response and an array of candidates. ICE PATCHes are authenticated with the same API key but are exempt from the per-account rate limiter because trickle ICE is time-sensitive and high-volume during connection setup.

Path & Query

1
  • session_idstring (UUID)required

    The session ID. Used for routing.

Request body

2
  • pc_idstringrequired

    PeerConnection identifier from the /offer response. Required on every PATCH.

  • candidatesIceCandidate[]required

    Array of ICE candidates. Each item has candidate (string), sdp_mid (string), and sdp_mline_index (integer). All field names are snake_case.

Errors

6
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

API key lacks the calls:write scope.

Use a key with the required scope.

400Peer connection not found

No active peer connection matches pc_id. The session likely ended or the pipeline crashed.

Restart the handshake by calling /connect then /offer again.

400Invalid ICE candidate

Malformed SDP fragment, invalid sdp_mid, etc.

Pass candidates through verbatim from RTCPeerConnection.onicecandidate; do not synthesize them.

422Invalid request body

Body fails validation (missing pc_id, malformed candidates array).

Match the shape exactly, including snake_case sdp_mid and sdp_mline_index.

502Upstream unavailable

The session host is temporarily unreachable.

Retry once. If it persists, restart the handshake.

WS/ws/v1/conversation/:session_id

Send the user's audio and receive the agent's, in real time.

Stream a call over WebSocket

The WebSocket your client opens to hold a real-time voice conversation with an agent. Connect to the ws_url returned by POST /conversation/connect with transport: "websocket". It is served outside the /api/v1 prefix, at wss://api.aethexai.com/ws/v1/conversation/{session_id}, and works like a signed URL: whoever holds it can join that one session, once.Messages are JSON text frames with a type. Your client sends audio (the user's audio), dtmf (keypad presses), contextual_update (background for the agent) and pong. It receives session_started, audio (the agent's audio), interruption (drop agent audio you have not played yet), user_transcript, agent_response, ping and call_ended. Audio is base64, both ways, in the session's audio_format: 16 kHz PCM by default, or µ-law at 8 kHz. With protocol: "twilio" the socket speaks the Twilio Media Streams format instead.By default the agent speaks first; with direction: "outbound" it lets the person who answered speak first, and greets them after their hello or after 3 seconds of silence. To hang up from your side, close the socket. Ending the call from your server with POST /calls/{call_id}/end or /end on this session sends your client call_ended. The full flow is in the WebSocket calls concept.

Path & Query

1
  • session_idstring (UUID)required

    The session_id returned by POST /conversation/connect for a websocket session.

Errors

3
1008Policy violation

The session is unknown or expired, was not opened with transport: "websocket", already has a socket, or has ended.

Open a new session with POST /conversation/connect and connect to its ws_url once.

1011Internal error

The call failed after it started. The session is marked failed.

Start a new session. If it keeps happening, contact support with the session_id.

1013Try again later

The call could not start: there was no capacity for it, or your account cannot be billed. An error event comes first, with code set to admission_rejected or billing_blocked.

For admission_rejected, retry with a new session after a short wait. For billing_blocked, check billing in your dashboard.

GET/conversation/:session_id/status

Inspect a session: status, duration, turn count. Works for active and recently-ended sessions.

Get Session Status

Returns the current status, wall-clock duration, and turn count of a session. Works for both active and recently-completed sessions. Auth is calls:read (read-only is sufficient). For a WebSocket session, status reads created until your client opens the socket, then in-progress, then completed, or failed if the call could not start.

Path & Query

1
  • session_idstring (UUID)required

    The session ID returned by /connect.

Errors

3
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

API key lacks the calls:read scope, or the session belongs to a different account.

Use a key with the required scope on the owning account.

404Session not found or expired

No record of this session: never created, expired, or evicted.

For historical records use GET /api/v1/conversations/{id} and GET /api/v1/calls/{id}.

POST/conversation/:session_id/end

Gracefully end an active session. Drains in-flight audio and runs post-call hooks.

End Session

Gracefully ends an active session. The agent finishes any in-flight TTS audio, then post-call hooks run (transcript finalization, recording upload, billing). Once this returns, the session ID is safe to use for transcript and recording lookups. This endpoint is idempotent for terminal sessions: if the session has already reached a terminal status (failed or completed) and is still retained in the session store (default 1 hour after termination), re-ending it returns 200 with the terminal snapshot (session_id, status, reason) — not an error. After session-store eviction, re-ending returns 404. Calling /end on a created session (reserved but never offered) transitions it to failed with reason: "abandoned_before_offer" and returns 200. On a WebSocket session, your client receives call_ended (stop with the twilio protocol) and the socket closes.

Path & Query

1
  • session_idstring (UUID)required

    The session ID.

Errors

5
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

Session is owned by a different account.

Use a key on the owning account.

404Session not found or expired

No record for this session: already ended or never existed.

Treat as already ended.

409Session not active (no live owner)

The session has no active pipeline owner and has not yet reached a terminal state (detail: "Session has no active pipeline owner and has not reached a terminal state (status='<'status'>')." ). Common case: session stuck in offering after a pipeline crash. A second detail string — "Session is not active. The pipeline may have already ended." — is returned on the stale-route path when the cached pod-route points back to the local pod but the pipeline task is already gone. Re-ending a terminal session does not return this — it returns 200 with the terminal snapshot.

The session is in a non-terminal limbo state with no live pipeline owner. Re-requesting /end will return the same 409 until the session self-transitions or is cleaned up. Use GET /conversation/{session_id}/status to monitor the state (requires the calls:read scope; an API key with only calls:write receives 403 Missing scope: 'calls:read'). Once the session reaches a terminal state, /end becomes idempotent and returns 200 with the terminal snapshot.

502Upstream unavailable

Session host is temporarily unreachable.

Retry once. If persistent, treat as already ended.

POST/conversation/:session_id/tool-result

Return a client-side tool execution result back to the agent and continue the conversation.

Submit Tool Result

Returns the result of a client-side tool invocation back to the agent. When the agent emits a tool call, your client executes it and POSTs the result here so the agent can continue. The result field must be a string, so stringify any structured payload before sending.

Path & Query

1
  • session_idstring (UUID)required

    The session ID.

Request body

2
  • tool_call_idstringrequired

    The tool_call_id from the agent's tool call. Required so the agent can pair the response with the originating call.

  • resultstringrequired

    The tool result, stringified. JSON payloads must be JSON.stringify-ed before sending.

Errors

6
401Unauthorized

Missing or invalid API key.

Send a valid X-API-Key header or bearer token.

403Forbidden

Session is owned by a different account.

Use a key on the owning account.

404Session not found or expired

No record for this session.

Verify the session ID.

409Session not active

Session is no longer running.

Submit the result before the session ends.

422Invalid body

tool_call_id or result field is missing or not a string.

Pass both fields as strings; stringify structured results.

502Upstream unavailable

Session host is temporarily unreachable.

Retry once.