If you've deployed an agent you built in Agentria as an API, external services can call its API endpoint to chat with the agent. The Agent API is a REST API that supports creating, listing, and deleting chat rooms, async chat, and SSE (Server-Sent Events) streaming chat.
This guide walks through how to call each endpoint and how to safely retrieve execution results while keeping conversation context.
The agent must already be deployed as an API. If you haven't deployed it yet, complete the 🔗API Release guide first.
You'll need the endpoint code (api_endpoint) and API key issued during deployment.
The base URL and authentication method apply to every endpoint below.
Base URL
Every request requires an X-API-KEY header.
If the API key is invalid or lacks permission, the API returns 403 Forbidden. Specific error codes can vary by endpoint, so check the Error Responses table in each step below.
Conversations with an agent are organized by chat room. To maintain conversation history, create a chat room first, then keep reusing the same chat room ID in subsequent requests.
Existing chat room ID (a new one is created if omitted)
input_files
list[string]
No
List of file URLs (HTTP URL or S3 URI)
If you don't specify chat_room_id, a new chat room may be created automatically for each request. To continue a conversation, keep passing the same chat room ID — the one you created in Step 1, or one you got back from a previous response.
external_request_id is an identifier the client issues before sending the request, and it serves two purposes. First, if the same external_request_id is submitted again, the server treats it as the same request and rejects it with 409 Conflict, which prevents duplicate execution (idempotency) from things like network retries. Second, because the client already holds this identifier before making the call, you can track and query the request by this value even before the server returns its own request_id in the response.
The X-Agent-Request-Id response header carries the request ID. Even if the connection drops before the first event arrives, you can use this value to re-query — it matches the first request_id event.
Note: The X-Request-Id header is a separate value used for HTTP tracing. Don't confuse it with X-Agent-Request-Id.
Each event is delivered in a self-defined SSE line format that is not compatible with the standard EventSource format. A blank line separates events.
In Agent API's SSE, every event except the initial request_id meta event is serialized wrapped once more in {"ability_node_id", "ability_node_name", "results"}. This differs from the Ability API's SSE, where the response/error event body arrives as the result schema itself — keep this in mind if you integrate both APIs together.
event_type
When it fires
Body shape
request_id
Right after the stream starts (first, once)
Plain string (UUID), delivered with no wrapping. The request ID to use for re-querying results if the stream is lost
node
Each time a node finishes executing (intermediate, repeats per node)
What matters here is that the final event has the full APIResponseSchema nested once more inside the results field (the same shape as the status query response in Step 3). So to check the final status, look at chunk.results.status rather than chunk.status.
If a network drop causes you to miss the final event, don't resend the same request — that would re-run the agent and duplicate the conversation history. Instead, re-query, since the server completes execution and records the result regardless of the stream connection.
Poll GET /{api_endpoint}/{request_id}/result using the request ID from the request_id event (or the X-Agent-Request-Id header) — see Step 3.
If you assigned an external_request_id, you can also query GET /{api_endpoint}/external/{external_request_id}/result — see Step 3.
Poll until status becomes COMPLETED, FAILURE, or CANCELED. Resending with the same external_request_id is rejected with 409 Conflict, so using an external ID also protects you against accidental duplicate runs.
constformData = newFormData();formData.append('params_json',JSON.stringify({input_message:'Hello',chat_room_id:'e36644fa-0b2e-11f0-b130-143627ec67e9'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/agent/my-agent/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// For re-querying via GET .../{requestId}/result if the stream is lost. Also updated by the first request_id event.letrequestId = response.headers.get('X-Agent-Request-Id');// A single read() can return multiple events merged together, so line-buffered parsing is required.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constchunks = [];// Accumulates every chunkletfinalChunk = null;while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;continue;}// event boundaryif(line.endsWith(':')){currentEvent = line.slice(0, -1);continue;}// The request_id event's body is a plain, unwrapped UUID string — handle it before JSON.parseif(currentEvent === 'request_id'){requestId = line;continue;}letbody;try{body = JSON.parse(line);}catch{continue;}chunks.push(body);// Intermediate vs. finalconststatus = body.results?.status;if(body.ability_node_id === 0 || status === 'COMPLETED' || status === 'FAILURE'){finalChunk = body;// The full APIResponseSchema lives inside resultsconsole.log('[final]',status,finalChunk.results.results);}else{console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}}}if(finalChunk?.results?.status === 'FAILURE'){console.error('Failed:',finalChunk.results.failure_reason);}// Stream ended without a final chunk — re-query instead of resending (re-running)if(!finalChunk && requestId){constres = awaitfetch(`${host}/api/agent/my-agent/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// Poll until status is COMPLETED or FAILURE}
constformData = newFormData();formData.append('params_json',JSON.stringify({input_message:'Hello',chat_room_id:'e36644fa-0b2e-11f0-b130-143627ec67e9'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/agent/my-agent/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// For re-querying via GET .../{requestId}/result if the stream is lost. Also updated by the first request_id event.letrequestId = response.headers.get('X-Agent-Request-Id');// A single read() can return multiple events merged together, so line-buffered parsing is required.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constchunks = [];// Accumulates every chunkletfinalChunk = null;while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;continue;}// event boundaryif(line.endsWith(':')){currentEvent = line.slice(0, -1);continue;}// The request_id event's body is a plain, unwrapped UUID string — handle it before JSON.parseif(currentEvent === 'request_id'){requestId = line;continue;}letbody;try{body = JSON.parse(line);}catch{continue;}chunks.push(body);// Intermediate vs. finalconststatus = body.results?.status;if(body.ability_node_id === 0 || status === 'COMPLETED' || status === 'FAILURE'){finalChunk = body;// The full APIResponseSchema lives inside resultsconsole.log('[final]',status,finalChunk.results.results);}else{console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}}}if(finalChunk?.results?.status === 'FAILURE'){console.error('Failed:',finalChunk.results.failure_reason);}// Stream ended without a final chunk — re-query instead of resending (re-running)if(!finalChunk && requestId){constres = awaitfetch(`${host}/api/agent/my-agent/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// Poll until status is COMPLETED or FAILURE}
constformData = newFormData();formData.append('params_json',JSON.stringify({input_message:'Hello',chat_room_id:'e36644fa-0b2e-11f0-b130-143627ec67e9'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/agent/my-agent/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// For re-querying via GET .../{requestId}/result if the stream is lost. Also updated by the first request_id event.letrequestId = response.headers.get('X-Agent-Request-Id');// A single read() can return multiple events merged together, so line-buffered parsing is required.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constchunks = [];// Accumulates every chunkletfinalChunk = null;while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;continue;}// event boundaryif(line.endsWith(':')){currentEvent = line.slice(0, -1);continue;}// The request_id event's body is a plain, unwrapped UUID string — handle it before JSON.parseif(currentEvent === 'request_id'){requestId = line;continue;}letbody;try{body = JSON.parse(line);}catch{continue;}chunks.push(body);// Intermediate vs. finalconststatus = body.results?.status;if(body.ability_node_id === 0 || status === 'COMPLETED' || status === 'FAILURE'){finalChunk = body;// The full APIResponseSchema lives inside resultsconsole.log('[final]',status,finalChunk.results.results);}else{console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}}}if(finalChunk?.results?.status === 'FAILURE'){console.error('Failed:',finalChunk.results.failure_reason);}// Stream ended without a final chunk — re-query instead of resending (re-running)if(!finalChunk && requestId){constres = awaitfetch(`${host}/api/agent/my-agent/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// Poll until status is COMPLETED or FAILURE}
Note (Python client): Parse line by line using httpx.AsyncClient.stream()'s aiter_lines() and JSON parsing, skipping node: / response: / error: prefix lines with a JSON parse error. The request_id event's body (a plain UUID) isn't JSON either, so it's skipped the same way — meaning an existing client's check like chunks[-1].get("results", {}).get("status") == "FAILURE" keeps working as-is. If you need the ID for re-querying, either track whether the previous line was request_id: or use the X-Agent-Request-Id response header.
Pick one of the three flows below depending on your use case.
Flow 1: Async + Polling
Flow 2: Async + Callback
Example callback payloads:
// Callback for a request with an external_request_id — key included{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}// Callback for a request without an external_request_id — key is omitted entirely{"request_id":"a1b2c3d4-...","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}
// Callback for a request with an external_request_id — key included{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}// Callback for a request without an external_request_id — key is omitted entirely{"request_id":"a1b2c3d4-...","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}
// Callback for a request with an external_request_id — key included{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}// Callback for a request without an external_request_id — key is omitted entirely{"request_id":"a1b2c3d4-...","chat_room_id":"e36644fa-...","status":"COMPLETED","results":{...},"failure_reason":null}
Requests without an external ID never include the external_request_id key in the callback payload — this preserves compatibility with existing integration code.
Flow 3: Real-Time Streaming
Note: If you don't specify chat_room_id, a chat room may be created automatically. To maintain conversation history, always use the same chat_room_id.
The Agent API lets external services chat with an agent you've built in Agentria via a REST API. It supports creating, listing, and deleting chat rooms, async chat, and SSE streaming chat, and every request is authenticated with an X-API-KEY header.
Use it when an external backend server or another service needs to chat with an agent and receive its responses without going through the Agentria UI. For example, it fits cases where you want to call an agent from your own chat interface, or build automated responses that maintain context across multiple turns of conversation.
The Ability API runs a completed workflow (ability) once and returns the result, while the Agent API exchanges multiple messages within a chat room while maintaining context. The SSE response shape also differs: in the Agent API, every event except the initial request_id event is wrapped in {ability_node_id, ability_node_name, results}, and you need to check results.status for the final status.
Context persists as long as you keep passing the same chat_room_id. If you don't specify a chat_room_id at request time, a new chat room may be created automatically, so to continue a conversation you need to pass the same chat_room_id you received in a previous response on every subsequent request.
The server keeps running the agent to completion and records the result regardless of the stream connection. If the connection drops before you receive the final event, don't resend the same request — poll the status/result query API using the request_id (or external_request_id) instead. Resending the same request re-runs the agent and can duplicate the conversation history.