Aria API
This page is for developers who want Aria, the revenexx assistant, inside their own app: a chat panel in a vendor app, a summary card on a dashboard, an assistant in an internal tool. It covers the HTTP API. If you write JavaScript or TypeScript, the Aria SDK wraps all of it, so read this page for the model and the SDK page for the code.
Aria answers as the signed-in user. Your app sends that user's token, Aria uses that same token for everything it does in the tenant, and you never create a credential for Aria itself. Two words recur below: a turn is one question and its answer, and a thread is a conversation of turns that Aria remembers.
Base URL and headers
https://aria.revenexx.ai/api
Aria has its own host. It is not behind the revenexx API gateway at api.revenexx.com, so the gateway's headers do not apply here: no X-Revenexx-Api-Key and no X-Revenexx-Tenant.
| Header | Required | Value |
|---|---|---|
Authorization | Yes, on every /v1/chat and /v1/threads call | Bearer <token>, a signed-in user's access token from ID |
X-Tenant-Id | When the user's organisation owns several tenants | The tenant to act on |
Accept-Language | No | en or de: the language of the answer and of Aria's own messages |
Content-Type | On POST /v1/chat and POST /v1/chat/stream | application/json |
Every response carries X-Api-Version, the Aria API version that served it. Read it from a server or with curl: browser code cannot see it, because Aria does not expose it to cross-origin scripts.
Aria accepts browser requests from any origin, so a single-page app can call it directly with the token it already holds for the signed-in user. That token is then in the browser, as it is for every other call your app makes as that user.
The API Explorer lists every operation under Aria. Its "Try it out" sends your request, with the token you paste in, through the portal's own proxy at revenexx.dev to this host, so use a short-lived token there.
Authentication
Aria takes the same kind of token your app already gets when a user signs in through ID. It does not take an API key: build your integration around a signed-in user.
The token has to meet three conditions, and a sign-in client set up for something else often misses one of them:
- It is a JWT, not an opaque token. Set your sign-in client's token type to JWT.
- It carries the user's organisation. A token without it fails every call with
401. - It is issued for the Revenue Cloud, and the user holds a Revenue Cloud role. Aria passes the token on to the model service, which checks both: without the audience the token is refused, and without a role the turn is refused with
403.
The second and third depend on scopes your sign-in client requests, and the audience names a project id that revenexx gives you. Ask for both when revenexx onboards your app: say that it will call Aria for signed-in users.
The token picks the organisation, not the tenant. X-Tenant-Id picks the tenant, and it must be one the organisation owns:
| What you send | Result |
|---|---|
| A tenant the organisation owns | That tenant |
| A tenant it does not own | 403 |
| No header, and the organisation owns one tenant | That tenant |
| No header, and the organisation owns several | 400: "Multiple tenants accessible; X-Tenant-Id header required" |
| No header, and the organisation owns none | 403 |
Send the header always if your users can belong to more than one tenant. It keeps the first multi-tenant customer from finding this out in production.
Quick start
Set TOKEN to a signed-in user's access token and TENANT to the tenant to act on. Create a thread, then send a turn into it:
curl -X POST https://aria.revenexx.ai/api/v1/threads \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT"
{ "thread_id": "8f6d2c1e-4b7a-4f0e-9a51-3c2d7e9b1a40" }
curl -X POST https://aria.revenexx.ai/api/v1/chat \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT" \
-H "Content-Type: application/json" \
-H "Accept-Language: en" \
-d '{"thread_id": "8f6d2c1e-4b7a-4f0e-9a51-3c2d7e9b1a40", "message": "How did revenue move last quarter?"}'
{
"response": "Revenue rose 8 % against the previous quarter …",
"usage": { "prompt_tokens": 2140, "completion_tokens": 182, "total_tokens": 2322 },
"follow_ups": [],
"tool_calls": [],
"stopped_reason": null,
"can_continue": false
}
Threads
A thread is one conversation. Aria keeps its history, so each turn sent with the same thread_id sees the turns before it.
- Leave
thread_idout for a stateless turn. Nothing is kept, and nothing can be continued later. Use it for a one-off answer such as a dashboard card. - A thread belongs to one user in one tenant. A turn into another user's thread, or one from another tenant, answers
404 not_found. Reading or deleting such a thread answers404too, the same as for a thread that never existed, so Aria never confirms that someone else's thread exists. - A turn into an unknown thread id starts that thread. A
thread_idthat is a valid UUID but unknown, including one that was deleted, is not an error onPOST /v1/chat: Aria registers it and answers with no history. To reopen an old conversation, read its messages first; a404there means it is gone. - A thread without activity for 30 days is deleted with its history, by a cleanup job, so it can outlive the 30 days briefly.
thread_idis a UUID. Anything else is refused with400.
| Operation | What it does |
|---|---|
POST /v1/threads | Creates a thread and returns { "thread_id": "…" }. |
GET /v1/threads | The user's threads, most recent activity first: thread_id, title, created_at and last_message_at (null until the first turn). |
GET /v1/threads/{thread_id}/messages | One thread's history: id, role, content, plus stopped on an answer that never finished. Add include_tool_calls=true to get the tool calls too. |
DELETE /v1/threads/{thread_id} | Deletes the thread and its history. Answers 204. |
Both lists page with a cursor. limit is 1 to 100 (default 20), and you pass a page's next_cursor back as cursor to get the next, older page; it is null on the last one. History pages run newest first, but the messages inside each page are in chronological order: render the first page, then put each older page above it.
Two kinds of caller cannot use threads. An impersonation token, an operator acting for a tenant, is refused on every thread call and on any turn with a thread_id, with 403 and details.reason: "impersonation_forbidden"; it can still send stateless turns. A token that names no user cannot create a thread (403, details.reason: "user_required").
Send a turn
POST /v1/chat returns the whole answer when it is finished. POST /v1/chat/stream runs the same turn and streams it as server-sent events. Both take the same body:
| Field | Type | Default | Meaning |
|---|---|---|---|
message | string | What the user asked. Must not be empty, except on a resume. | |
thread_id | string or null | null | The thread to continue. Leave it out for a stateless turn. |
model | string or null | null | A model id from GET /v1/models. Leave it out for Aria's default. |
resume | boolean | false | Continue a paused turn. Needs thread_id and an empty message. |
include_follow_ups | boolean | false | Also suggest next questions. Off by default because it costs a second model call. |
The buffered answer has these fields:
| Field | Meaning |
|---|---|
response | The answer text. Empty when the turn paused. |
usage | prompt_tokens, completion_tokens and total_tokens for the turn. |
follow_ups | Suggested next questions, { "question", "starred" }, when you asked for them. |
tool_calls | The tools Aria called to answer: tool, subject, args, result and ok. The display label and icon come only on the stream. |
stopped_reason | step_limit when the turn paused, otherwise null. |
can_continue | true when a paused turn can be resumed. |
Use the buffered call when there is nowhere to show partial text. For a chat panel, stream: an answer that calls several tools can take a while, and the stream shows the user what is happening meanwhile.
Aria answers in the language you ask for. Accept-Language supports en and de, and a regional tag counts as its language (de-AT is de). It sets the language of the answer, the follow-up questions, the pause message and Aria's own error messages. Errors from the sign-in and tenant checks, and from malformed ids or cursors, are always in English. Without the header, or with a language Aria does not support, the answer follows the language the user wrote in.
Streamed answers
POST /v1/chat/stream answers 200 with Content-Type: text/event-stream. Each frame is a named event with a JSON data line (the values here are illustrative):
event: status
data: {"tool": "orders_search", "run_id": "run_1", "label": "Searching orders"}
event: status
data: {"state": "generating"}
event: delta
data: {"content": "Revenue rose 8 %"}
event: delta
data: {"content": " against the previous quarter."}
event: done
data: {"usage": {"prompt_tokens": 2140, "completion_tokens": 182, "total_tokens": 2322}}
| Event | data | Meaning |
|---|---|---|
delta | { "content" } | The next piece of the answer. Append in order; the full answer is never sent again. |
status | { "tool", "run_id", "label" }, optionally icon and args | A tool call started. Its run_id is the id of the tool_trace entry that closes it. |
status | { "state": "generating" } | A tool finished and the model is writing again. |
tool_trace | { "entry" } | A tool call ended, with its arguments, result and ok. Always before done. |
done | { "usage" } | The answer is complete. |
follow_ups | { "follow_ups": [{ "question", "starred" }] } | Suggested next questions. Only with include_follow_ups, and only after done. |
paused | { "reason", "message", "usage" } | The turn ran out of steps. The stream closes. See Paused turns. |
error | { "error", "message", "details" } | The turn failed after the stream opened. The stream closes. |
Three things catch people out:
doneis not always the last event. Withinclude_follow_ups,follow_upscomes after it. Close your UI's "thinking" state ondone, and the connection when the server closes it.- The stream has no heartbeat. Nothing arrives while a long tool call runs, so a gap is not a dead connection. Don't build a liveness timeout on frame arrival.
- Treat an unknown event name as something to skip, not an error. New events are additions.
Everything Aria checks up front, it refuses before the stream opens: a bad token, a wrong tenant, AI switched off, a thread that isn't the user's. Those come back as ordinary HTTP errors with the error body, not as an error event. Whatever goes wrong once the turn is running arrives as an error event after a 200: a failed model call, an unknown model, an empty wallet, or a stateless turn that hit the step limit.
Tool icons come from each tool's own metadata. Treat the icon value as untrusted: render it only as an image source, never as HTML.
Paused turns
A turn has a limit on how many steps (model calls and tool calls) it may take. A question that needs more pauses instead of failing, and the thread keeps the work done so far.
- From the stream, a
pausedevent arrives, with amessageyou can show the user, and the stream closes. - From
POST /v1/chat, the answer is200with an emptyresponse,stopped_reason: "step_limit"andcan_continue: true.
To continue, send the same thread with resume: true and no message:
{ "thread_id": "8f6d2c1e-4b7a-4f0e-9a51-3c2d7e9b1a40", "resume": true }
Only a turn in a thread can pause. A stateless turn that hits the limit has nothing to keep its progress in, so it fails with internal_error and a message asking the user to narrow the question: a 500 from POST /v1/chat, an error event from the stream. The limit itself is not part of the contract, so design for the pause rather than for a number of steps.
Models
GET /v1/models lists the model ids a turn can ask for, the one Aria uses when you send none, and a relative cost per model (1 is the cheapest). It needs no token, so you can fill a model picker before the user signs in.
curl https://aria.revenexx.ai/api/v1/models
{
"default": "…",
"models": ["…", "…"],
"costs": { "…": 2 }
}
The list changes as models are added and retired, so read it at runtime instead of hard-coding an id. Aria does not check the id you send against the list: the model service refuses an unknown one, as 400 bad_request from POST /v1/chat and as an error event from the stream.
GET /health answers { "status": "ok" } without a token. It says the service is up, not that a turn will succeed.
Errors
Every error has the same body:
{ "error": "forbidden", "message": "…", "details": null }
Branch on error, show message, and read details where the table names it.
| Status | error | What happened | What to do |
|---|---|---|---|
400 | bad_request | A malformed thread_id, an invalid or outdated cursor, no X-Tenant-Id when the organisation has several tenants, or a model the model service does not know. | Fix the request. For a cursor, start again from the first page. |
401 | unauthorized | No token, an expired one, or one that fails the three conditions, checked by Aria or by the model service. | Refresh the token once and retry. A 401 that survives a fresh token is a token set-up problem, not an expiry. |
403 | forbidden | The tenant is not the organisation's, or the model service refused the user, for example for a missing Revenue Cloud role. With details.reason, the caller may not use threads. | Don't retry. |
403 | feature_disabled | The tenant has switched AI off in its settings. details is { "feature": "ai" }. Refused before any model call. | Hide the assistant for this tenant. Nothing the client sends changes it. |
404 | not_found | The thread isn't this user's in this tenant, or, when reading or deleting, doesn't exist. | Treat the thread as gone. |
422 | unprocessable_entity | The request is invalid: an empty message, resume without thread_id or with a message, or a limit outside 1 to 100. details.errors lists each problem. | Fix the request. |
429 | rate_limited | The model service is rate-limiting. | Back off before you retry. |
500 | internal_error | A stateless turn hit the step limit, or an unexpected failure. | Narrow the question, or retry later. |
502 | bad_gateway | The model call failed. This includes a tenant whose AI credit wallet is empty: the message then names upstream status 402. | Show "not available right now". Don't retry automatically. |
503 | unavailable | A service Aria depends on is down. If AI could not be confirmed as switched on, Aria refuses rather than guess, and details is { "feature": "ai" }. | Retry later. |
There is no dedicated code for an empty wallet yet (as of 2026-10), which is why it arrives as bad_gateway. The same codes arrive in a stream's error event when the failure happens mid-turn.
A refusal from the model service keeps its own status, so 408 request_timeout and 409 conflict can occur too, and an unexpected status arrives with the code error. Handle unknown codes by showing message.
What Aria can do in a tenant
Aria answers by calling tools against the tenant, and the stream's status and tool_trace events show each call as it happens. Every call carries the token you sent. Plan for Aria to be able to do whatever that user's token allows, writes included, rather than for a narrower set: the user's own permissions are the boundary, so a user who can change data can ask Aria to change it. Before you put Aria in front of a customer's users, read What an AI agent can change, which applies the same reasoning to every credential an agent holds.
Where to go next
- Aria SDK: the typed client for JavaScript and TypeScript, with streaming, pauses and errors handled.
- API Explorer: every Aria operation under Aria, with "Try it out".
- Authentication: how users sign in through ID.