aethexDocs

API Reference

Conversation (WebRTC) 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.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
7
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.

Request body

2
  • 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 left as written. 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. Unlike POST /calls/trigger, an unsupported system__ placeholder is left as written 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.

Errors

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

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.

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.

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

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

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.

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

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.

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.