C3 AI Agentix Developer Docs
The Agent REST API lets your own code drive an Agentix agent outside of the C3 AI Agentix application UI. Programmatic callers such as CI jobs, batch workflows, and other C3 applications can create an agent session, send a prompt, read the streamed answer, exchange files, and manage the session lifecycle, all over HTTP under a versioned /agent-api/v1/ surface.
This page is a task guide: authenticate, then run one session end to end.
How the API is shaped
The surface is split across two planes, and every integration works with both.
| Plane | Where it runs | What it carries |
|---|---|---|
| Control plane | The C3 AI Agentix app gateway, at <agentixBaseUrl>/agent-api/v1/ | Low-volume lifecycle and discovery: create, list, get, stop, resume, delete, bind app, definitions, models, status. |
| Data plane | The per-session agent pod, at <podUrl>/ | High-volume traffic: messages, the event stream, and files. |
When you create a session, the control plane returns that session's pod_url. You then send messages, read events, and move files by calling the pod at that URL directly. The C3 AI Agentix app isn't on the per-message path, so throughput scales with the number of pods instead of a single app.
The browser can't call the pod directly
A pod validates a caller's C3 bearer token, but its cross-origin rules don't allow an Authorization header from another web origin, and a browser event stream can't set that header at all. So a browser-based integration must send every pod call through its own server-side proxy, which holds the token and forwards to C3 AI Agentix. A server or command-line caller has no such restriction and calls the pod directly. A chat page inside a C3 application on the same cluster host avoids the problem entirely: see Embed a chat in a C3 app.
Before you begin
You need:
- A reachable C3 AI Agentix environment and its base URL, ending with a slash, for example
https://<agentix-host>/<tenant>/nexus/. - The
Agentix.Userrole on that environment.
Which further setup you need depends on where your caller runs — see Authenticate.
Authenticate
How you authenticate depends on where your caller runs and whose identity C3 AI Agentix should see:
| Your caller... | Use... | Identity C3 AI Agentix sees |
|---|---|---|
| Runs as a C3 application in the same environment as C3 AI Agentix | The platform's own session-token exchange | The signed-in human, on their existing session |
| Runs anywhere, with no human behind it | client_credentials | A service identity (the OAuth client) |
| Runs in a different environment, acting as a specific signed-in human | authorization_code with PKCE | The signed-in human, via a one-time OAuth exchange |
Same environment: exchange your own session token
When your integration's own code already runs as a C3 application in the same environment as C3 AI Agentix, you need no OAuth at all. This is the common case when the caller is "another C3 application." C3 AI Agentix and your app share the same session store, so any caller already authenticated to your app can mint the platform's own self-scoped session token and present it directly:
AGENTIX="https://<agentix-host>/<tenant>/nexus"
TOKEN=$(curl -sS -u "$USER:$PASSWORD" -X POST "https://<your-app-host>/<env>/<your-app>/api/8/User/mySessionToken" \
-H 'Content-Type: application/json' -d '{}' | grep -o '"signedToken":"[^"]*"' | cut -d'"' -f4)
curl -sS "$AGENTIX/agent-api/v1/sessions/list" -H "Authorization: c3auth $TOKEN"User.mySessionToken is self-scoped: it only ever returns the caller's own token, needs no role grant beyond Agentix.User, and needs nothing registered on the C3 AI Agentix environment. The platform's authenticator accepts Authorization: c3auth <token> the same way it accepts Authorization: Bearer <token>, so every other example on this page works unchanged with either. The token's lifetime tracks your own C3 session; if a call starts returning 401, mint a fresh one the same way.
The two flows below are for a caller in a different environment. Both need one prerequisite first: register an OAuth application on the C3 AI Agentix environment once, scoped to Agentix.User. This issues the client credentials the flow authenticates with. Run this in a console on the C3 AI Agentix environment:
var app = OAuthApplication.make({
name: 'myIntegration',
redirectUri: 'https://<your-app-base-url>/',
homePageUrl: 'https://<your-app-base-url>/',
description: 'External client for Agentix agents.',
});
var creds = app.register(['Agentix.User']); // returns { clientId, clientSecret }Capture creds.clientId and store creds.clientSecret securely: the secret is shown once. The redirectUri matters only for the per-user flow below; a headless caller can leave it as the app home URL. On platform 8.10 and later the type is named OAuth.Application; on 8.9 it's OAuthApplication. The rest of this section works the same on both.
Headless caller: client_credentials
AGENTIX="https://<agentix-host>/<tenant>/nexus"
curl -sS -X POST "$AGENTIX/oauth/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET"
# -> {"access_token":"<token>","token_type":"bearer"}The client credentials must appear in both the HTTP Basic header (-u) and the form body (-d). Sending them in only one place returns 401. The access token defaults to a two-hour lifetime and has no refresh token, so re-mint it when it expires.
Present the token on every control-plane and data-plane call as an Authorization header:
Authorization: Bearer <access_token>External signed-in user: authorization_code with PKCE
Use this flow when your caller runs in a different environment, or isn't a C3 application at all. It's for when the agent should act as a specific signed-in person (their C3 AI Agentix roles, their own sessions) instead of as a shared service identity. It runs in three steps, and only the first needs a browser.
Authorize in the browser. Send the user's browser to the C3 AI Agentix authorize endpoint with a random
stateand a PKCEcode_challenge. After the user signs in, C3 AI Agentix redirects the browser back to your registeredredirectUriwithcodeandstateon the query string.Text<agentixBaseUrl>/oauth/authorize?response_type=code&client_id=<clientId> &redirect_uri=<redirectUri>&scope=&state=<state> &code_challenge=<challenge>&code_challenge_method=S256The
stateis an opaque random value you generate and re-check on return, to guard against cross-site request forgery. Thecode_challengeis the base64url SHA-256 of a randomcode_verifierthat you keep server-side for step 2. Theredirect_urimust match the value registered on the OAuth application byte for byte.Exchange the code for tokens. Your server-side code swaps the returned
code, together with the storedcode_verifier, for tokens. Send the client credentials in both the Basic header and the body, the same gate as the headless flow.Command Linecurl -sS -X POST "$AGENTIX/oauth/token" \ -H 'Content-Type: application/x-www-form-urlencoded' -u "$CLIENT_ID:$CLIENT_SECRET" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "code=$CODE" \ --data-urlencode "redirect_uri=$REDIRECT_URI" \ --data-urlencode "code_verifier=$CODE_VERIFIER" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" # -> {"access_token":"<token>","token_type":"bearer","refresh_token":"<token>"}Refresh when the token expires. This flow returns a
refresh_token, so you renew the access token without sending the user back through sign-in.Command Linecurl -sS -X POST "$AGENTIX/oauth/token" \ -H 'Content-Type: application/x-www-form-urlencoded' -u "$CLIENT_ID:$CLIENT_SECRET" \ --data-urlencode "grant_type=refresh_token" \ --data-urlencode "refresh_token=$REFRESH_TOKEN" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET"Present the resulting access token the same way as the headless flow.
Read authentication errors
Two status codes cover every authentication failure, whichever path above you used:
401means the credential is missing, rejected, or expired. Mint a fresh session token (same environment), re-mint (headless), or refresh (external signed-in user).403means the caller lacks theAgentix.Userrole or doesn't own the session.
Match on the HTTP status, not the response body: the platform doesn't return a machine-readable error code and doesn't distinguish an expired token from a rejected one.
Run a session end to end
This section is one complete round trip with a headless token: create a session, wait for it to be ready, send a prompt, read the reply, exchange files, then stop. Set these first:
AGENTIX="https://<agentix-host>/<tenant>/nexus"
TOKEN="<access_token from the previous step>"1. Create a session
curl -sS -X POST "$AGENTIX/agent-api/v1/sessions/create" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"agent_name":"default"}'
# -> 202 { "id": "<sessionId>", "status": "STARTING",
# "pod_url": "https://<agentix-host>/.../cloud-agent/<sessionId>" }The response gives you the id (the session) and the pod_url (the data-plane base). A STARTING status means the pod is still warming; a cold start can take a few minutes. The Retry-After header tells you how long to wait before polling. agent_name selects an agent definition; default is the built-in agent, and Discover agents, models, and status lists the rest.
2. Wait until the session is ready
SESSION_ID="<id from the create response>"
curl -sS "$AGENTIX/agent-api/v1/sessions/get/$SESSION_ID" \
-H "Authorization: Bearer $TOKEN"
# -> { "status": "READY", "pod_url": "..." }Poll sessions/get until status is READY or ACTIVE. Honor the Retry-After header on STARTING responses instead of polling in a tight loop. A terminated or removed session returns 410 Gone.
Capture the pod URL for the data-plane calls that follow:
POD_URL="<pod_url from the get response>"3. Send a message
curl -sS -X POST "$POD_URL/agents/v1/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"prompt":"List the wind turbines with the lowest availability."}'
# -> 202 { "turn_id": "<turnId>" }Message and event paths on the pod sit under /agents/v1/, not /agent-api/v1/. Sending is asynchronous by default: you get a turn_id and read the result from the event stream in the next step. For a short turn, add ?wait=true (or the header Prefer: wait=<seconds>, up to 300) to hold the connection and get the finished turn inline as 200 { turn_id, status, events }. Only one turn runs per session at a time; a second send while one is running returns 409.
4. Read the agent's reply
Poll the event stream, paging forward by event id:
curl -sS "$POD_URL/agents/v1/sessions/$SESSION_ID/events?start_id=0" \
-H "Authorization: Bearer $TOKEN"
# -> { "events": [ ... ], "next_start_id": 42, "status": "idle", "stream_epoch": "..." }The event stream is the canonical transcript. Each event has a monotonic id; on the next poll set start_id to the next_start_id you were given. The agent's text lives in events whose kind is MessageEvent, in the content array. A status of idle means the turn has finished.
For an interactive caller, stream the events instead of polling:
curl -sS -N "$POD_URL/agents/v1/sessions/$SESSION_ID/events?stream=true" \
-H "Authorization: Bearer $TOKEN"With stream=true the pod returns a text/event-stream. Resume a dropped stream with the Last-Event-Id header (or the last_event_id query parameter). If the stream_epoch value changes, the pod restarted and renumbered its events, so re-sync from start_id=0.
5. Exchange files
File operations run against the pod under the /agent-api/v1/ base and work as documented.
| Operation | Call |
|---|---|
| List | GET <podUrl>/agent-api/v1/sessions/files/<sessionId> |
| Upload | POST <podUrl>/agent-api/v1/sessions/files/<sessionId> (multipart, field file) |
| Preview or view | GET <podUrl>/agent-api/v1/sessions/file-view/<sessionId>?path=<path> |
| Download one file | GET <podUrl>/agent-api/v1/sessions/file/<sessionId>?path=<path> |
| Download the workspace | GET <podUrl>/agent-api/v1/sessions/files-zip/<sessionId> |
# Upload an input file
curl -sS -X POST "$POD_URL/agent-api/v1/sessions/files/$SESSION_ID" \
-H "Authorization: Bearer $TOKEN" -F "file=@./input.csv"
# Download everything the agent produced
curl -sS "$POD_URL/agent-api/v1/sessions/files-zip/$SESSION_ID" \
-H "Authorization: Bearer $TOKEN" -o workspace.zipRunning a shell command in the workspace (sessions/<sessionId>/exec) is available only on the hosted OpenHands runtime; the in-pod agent returns 501 and advertises its supported operations in a capabilities object. Use the file operations or the agent's own tools instead.
6. List, stop, resume, and delete
# List your sessions
curl -sS "$AGENTIX/agent-api/v1/sessions/list" -H "Authorization: Bearer $TOKEN"
# Stop the pod, keeping its files (status becomes PAUSED)
curl -sS -X POST "$AGENTIX/agent-api/v1/sessions/stop/$SESSION_ID" -H "Authorization: Bearer $TOKEN"
# Resume a paused pod
curl -sS -X POST "$AGENTIX/agent-api/v1/sessions/resume/$SESSION_ID" -H "Authorization: Bearer $TOKEN"
# Soft-remove this chat thread
curl -sS -X DELETE "$AGENTIX/agent-api/v1/sessions/delete/$SESSION_ID" -H "Authorization: Bearer $TOKEN"Stop and resume act on the whole pod, so every session on it pauses or wakes together. Delete removes one chat thread, and you can't delete the last chat on a pod. A paused session keeps its files and transcript on the pod's disk; deleting the session terminates it and loses them, so export anything you need with files-zip first.
Embed a chat in a C3 app
The most common integration is a chat page inside an existing C3 application. Don't build this on the Agent REST API: a consumer app on the same cluster host as C3 AI Agentix drops in the NexusChatEmbed component instead (contact your C3 AI representative for setup details). It needs no server-side piece at all: no proxy, no bearer token to hold or refresh, no redirect URI to register. The chat rides the page's existing sign-in through a one-time silent cookie exchange, so every call runs as the signed-in end user.
NexusChatEmbed replaces the superseded approach, in which a server-side proxy held a bearer token and forwarded every call. Don't reintroduce that proxy for a same-cluster embed.
Discover agents, models, and status
These control-plane calls tell a caller what's available and how its pod is doing.
| Call | Returns |
|---|---|
GET /agent-api/v1/definitions | The agent definitions you can pass as agent_name on create. |
GET /agent-api/v1/models | The LLM models you can request for a session. |
GET /agent-api/v1/users/me | Your identity and your active-session count. |
GET /agent-api/v1/status | The health of your current service, including whether its pod is reachable. |
The full machine-readable contract is served live at GET /agent-api/v1/openapi.json and can be viewed with any OpenAPI tool such as Swagger UI or ReDoc.
Troubleshooting
Match the symptom you're seeing to its likely cause and fix:
| Symptom | Likely cause and fix |
|---|---|
401 on every token request | The client credentials aren't in both the Basic header and the body. Send them in both. |
OAuth endpoints return 404 | agentixBaseUrl is missing its trailing slash. |
Bad request after the user signs in | The redirect URI doesn't match the value registered on the OAuth application byte for byte. |
state mismatch on the callback | The state you generated no longer matches or has expired on your side. Retry sign-in. |
401 on an API call | The token expired. Re-mint it (headless) or let the refresh run (per-user). |
403 on an API call | The caller lacks the Agentix.User role, or the session belongs to another user. |
| First message hangs or times out | A cold start can take a few minutes. Poll sessions/get and honor Retry-After. |
429 with a Retry-After header | You reached the per-pod rate limit or the concurrent-turn cap. Back off for the stated interval. |
create, list, or a paused get misbehaves | See the current-build notes in Run a session end to end. |