One intake. Three calls. A clinician-ready result.
The Auddax API runs a governed clinical intake. You create an intake, you send each patient message as a turn, and you read the clinician handoff when the intake completes. The engine decides the questions, the safety status, and the disposition. Your application owns the surface.
The model is simple. An intake is one patient conversation. A turn is one patient message and the engine's reply. The handoff is the structured clinical summary the intake produces. Send patient language in. Get structured clinical state out.
https://api.auddax.ai
Three rules keep your integration safe. Relay assistant_message to the patient without edits.
Never override safety_status or disposition. Stop sending turns when
outcome is no longer continue; terminal is then true.
Evaluating the engine with an AI agent? Start at Testing with an agent. The agent guide is plain text at api.auddax.ai/llms.txt.
From key to handoff in three calls.
You need an API key. Create an account in the developer console and generate one in a few clicks, or request one by email. Export it before you run the examples.
Step 1 · Create an intake
Create the encounter first. The response gives you the encounter_id you use on every later
call. The optional fields seed clinical context before the first turn.
$ curl -X POST https://api.auddax.ai/v1/intakes \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "quickstart",
"demographics": { "age_years": 58, "sex": "male" },
"patient_history": "Allergies: penicillin. Current medications: lisinopril."
}'
import os, httpx
BASE = "https://api.auddax.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['AUDDAX_API_KEY']}"}
intake = httpx.post(
f"{BASE}/v1/intakes",
headers=HEADERS,
json={
"label": "quickstart",
"demographics": {"age_years": 58, "sex": "male"},
"patient_history": "Allergies: penicillin. Current medications: lisinopril.",
},
timeout=60,
).json()
encounter_id = intake["encounter_id"]
const BASE = "https://api.auddax.ai";
const HEADERS = {
Authorization: `Bearer ${process.env.AUDDAX_API_KEY}`,
"Content-Type": "application/json",
};
const intake = await fetch(`${BASE}/v1/intakes`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
label: "quickstart",
demographics: { age_years: 58, sex: "male" },
patient_history: "Allergies: penicillin. Current medications: lisinopril.",
}),
}).then((r) => r.json());
const encounterId = intake.encounter_id;
{
"encounter_id": "enc_auddax-sandbox_91a81eb05e4c58fa",
"status": "open",
"label": "quickstart",
"metadata": {},
"mode": "standard",
"created_at": "2026-11-03T16:31:04.118Z",
"expires_at": "2026-11-03T18:31:04.000Z",
"config": { "locale": "en-US" },
"config_hash": "6c0570a2ff7e1192"
}
Send each patient message as one turn. In curl, set ENCOUNTER_ID to the
encounter_id from step 1. The engine returns the next patient-facing message and the current
clinical state. This example triggers a cannot-miss pathway, so the turn is terminal and the handoff is
included.
$ curl -X POST https://api.auddax.ai/v1/intakes/$ENCOUNTER_ID/turns \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: turn-1-$ENCOUNTER_ID" \
-d '{
"message": "Chest pressure for the last hour, sweating and short of breath."
}'
turn = httpx.post(
f"{BASE}/v1/intakes/{encounter_id}/turns",
headers={**HEADERS, "Idempotency-Key": f"turn-1-{encounter_id}"},
json={"message": "Chest pressure for the last hour, sweating and short of breath."},
timeout=240,
).json()
print(turn["assistant_message"])
if turn["terminal"]:
handoff = turn["handoff"]
const turn = await fetch(`${BASE}/v1/intakes/${encounterId}/turns`, {
method: "POST",
headers: { ...HEADERS, "Idempotency-Key": `turn-1-${encounterId}` },
body: JSON.stringify({
message: "Chest pressure for the last hour, sweating and short of breath.",
}),
}).then((r) => r.json());
console.log(turn.assistant_message);
const handoff = turn.terminal ? turn.handoff : null;
{
"encounter_id": "enc_auddax-sandbox_91a81eb05e4c58fa",
"turn_index": 1,
"assistant_message": "I need you to call 911 right now or have someone take you
to the emergency room immediately. [...]",
"choices": [],
"terminal": true,
"urgent": true,
"outcome": "urgent",
"terminal_reason": null,
"safety_status": "cannot_miss_positive",
"disposition": "urgent_escalation",
"disposition_label": "Emergency evaluation",
"handoff": { "soap": { "...": "..." }, "provenance": { "...": "..." } }
}
A benign message behaves differently. The engine asks the next question, outcome is
continue, terminal stays false, and choices can carry quick replies
you can render as buttons. The interview closes only when the patient says it has nothing more to add, for
example “That's all. I have nothing more to add.” There is no turn cap.
A turn takes 10 to 40 seconds, and a closing turn can take longer. See latency for timeouts, and idempotency and retries before you retry a turn.
Step 3 · Read the handoff
The handoff stays readable by encounter_id after the intake closes and after the session
expires. Store the id, not the payload, if you want to fetch it later.
$ curl https://api.auddax.ai/v1/intakes/$ENCOUNTER_ID/handoff \
-H "Authorization: Bearer $AUDDAX_API_KEY"
handoff = httpx.get(
f"{BASE}/v1/intakes/{encounter_id}/handoff",
headers=HEADERS,
timeout=60,
).json()
print(handoff["disposition"])
print(handoff["clinical_assessment"])
const handoff = await fetch(`${BASE}/v1/intakes/${encounterId}/handoff`, {
headers: HEADERS,
}).then((r) => r.json());
console.log(handoff.disposition);
console.log(handoff.clinical_assessment);
Read disposition for triage and clinical_assessment.working_diagnosis for the
diagnosis. Which field to read has the details. The full
handoff shape is documented in the handoff object.
Let an agent play the patient.
You can evaluate the engine with an AI agent. The agent plays a synthetic patient, sends one message per turn, and judges what the engine asks and how it closes. Point the agent at two machine-readable sources. They describe the same contract as these docs.
The evaluation loop
| Step | Call | What to do |
|---|---|---|
| 1 · Create | POST /v1/intakes |
Send a label (for example the scenario id) and optional metadata (for example the arm and the run). Unknown fields return 400 invalid_request, so a typo fails loudly. |
| 2 · Turn | POST /v1/intakes/{encounter_id}/turns |
Send one patient message with a new Idempotency-Key. Read assistant_message, choices, choice_mode, and outcome. |
| 3 · Stop | None | Repeat step 2 while outcome is continue. When the scenario runs out, the patient says it has nothing more to add. Stop on complete, urgent, or not_eligible. Set your own turn limit too, so a run that never closes still ends. |
| 4 · Result | GET /v1/intakes/{encounter_id}/result |
One flat object to assert on: outcome, disposition, safety status, protocol, red flags, clinical assessment, and provenance. /handoff returns the full handoff and transcript. |
| 5 · List | GET /v1/intakes?label=… |
Collect your runs, newest first, with outcome and turn count. Free. |
import os, time, uuid, httpx
http = httpx.Client(
base_url="https://api.auddax.ai",
headers={"Authorization": f"Bearer {os.environ['AUDDAX_API_KEY']}"},
timeout=240, # a turn takes 10 to 40 s
)
RETRY = {"turn_in_progress", "upstream_error", "unavailable", "rate_limited"}
# Not retried: 504 upstream_timeout. The engine may still answer; see "Idempotency and retries".
def error_code(r):
try:
return r.json()["error"]["code"]
except (ValueError, KeyError, TypeError): # a proxy 502 can have a body that is not JSON
return "upstream_error" if r.status_code == 502 else None
def turn(encounter_id, message):
key = str(uuid.uuid4()) # one key per patient message
for _ in range(8):
try:
r = http.post(f"/v1/intakes/{encounter_id}/turns",
headers={"Idempotency-Key": key}, json={"message": message})
except httpx.TransportError:
time.sleep(10)
continue # same key: never a second message
if r.status_code == 200:
return r.json()
if error_code(r) not in RETRY:
r.raise_for_status()
time.sleep(int(r.headers.get("Retry-After", "10")))
raise RuntimeError("the turn did not complete")
intake = http.post("/v1/intakes", json={
"label": "sore-throat-01",
"metadata": {"arm": "default", "run": "1"},
"demographics": {"age_years": 34, "sex": "female"},
}).json()
eid = intake["encounter_id"]
reply = turn(eid, "I have had a sore throat for two days.")
while reply["outcome"] == "continue" and reply["turn_index"] < 12:
# Your agent answers as the patient. When the scenario runs out, it says
# "That's all. I have nothing more to add." The engine closes only on that.
reply = turn(eid, play_patient(reply))
result = http.get(f"/v1/intakes/{eid}/result").json()
print(result["outcome"], result["disposition"], result["clinical_assessment"], result["provenance"]["source_commit"])
Retry rules
A retry must never send a patient message twice. The Idempotency-Key is claimed before the turn
runs, and one turn per intake produces its reply at a time.
| When | Do this |
|---|---|
| Client timeout, network error, or a 502 from the proxy | Retry the same request with the same Idempotency-Key. You get the stored result, or 409 turn_in_progress while the first request still runs. |
| A dropped stream | A stream has no key. Read GET /v1/intakes/{encounter_id} before you send anything. |
409 turn_in_progress | Wait Retry-After seconds, then retry with the same key. Retries count against the per-minute limit, so poll the free GET /v1/intakes/{encounter_id} for turn_in_progress and last_turn while you wait. Without a key, this code means another turn on the intake is still producing its reply and nothing was sent. |
504 upstream_timeout | The turn may still finish. Do not retry the key. Wait until GET /v1/intakes/{encounter_id} shows turn_in_progress: false, then read the handoff conversation: if your message is there, continue from the engine's reply; if not, send it with a new key. Note the encounter_id in your findings. |
409 turn_outcome_unknown | You retried a key whose request timed out. Do the same as for 504. |
502 upstream_error · 503 unavailable | Retry later with the same key. A failed turn frees its key. |
429 rate_limited | Wait Retry-After seconds. |
429 quota_exceeded | Stop. The daily quota resets at 00:00 UTC, and Retry-After points there. |
Timeouts. A turn takes 10 to 40 seconds, and a closing turn can take longer. Set the agent's HTTP timeout to at least 300 seconds. Many agent tool runners default to 30 or 60 seconds, which cuts turns off mid-reply. See latency for what happens at 120 seconds.
Report findings with the build. Quote engine.wrapper_build_sha from
GET /v1/health and provenance.source_commit from the result, plus the
encounter_id and audit_ref. The defaults the engine runs with are described in
Engine behavior. For fixed scripts that do not adapt to
the engine's questions, batches run server side.
Prefer to test from a chat? An Auddax MCP server runs the same loop from Claude Desktop or Claude Code. Your Auddax contact sends it to you with your key.
Typed clients for Python and TypeScript.
Version 0.5.0 of the official SDKs covers the standard intake surface with typed requests, responses, and
errors. Streaming, retries, idempotency keys, and webhook signature verification are built in, so what you
test is the contract, not your HTTP plumbing. Both clients read AUDDAX_API_KEY from the
environment. For routes outside the standard intake surface, call the HTTP API directly; the
endpoint list and the spec name every route.
$ pip install auddax # Python 3.10+
$ npm install auddax # Node 20+, zero dependencies
The whole quickstart above, through the SDK. A blocking turn attaches an Idempotency-Key
automatically, streaming is an iterator, and terminal turns embed the typed handoff.
from auddax import Auddax
client = Auddax()
intake = client.intakes.create(demographics={"age_years": 58, "sex": "male"})
with client.intakes.stream_turn(intake.encounter_id, "I have chest pressure and I'm sweating.") as stream:
for fragment in stream.text():
print(fragment, end="", flush=True)
if stream.turn.terminal:
print(stream.turn.handoff.soap.assessment)
import { Auddax } from "auddax";
const client = new Auddax();
const intake = await client.intakes.create({ demographics: { age_years: 58, sex: "male" } });
const stream = await client.intakes.streamTurn(intake.encounter_id, "I have chest pressure and I'm sweating.");
for await (const fragment of stream.text()) {
process.stdout.write(fragment);
}
if (stream.turn?.terminal) {
console.log(stream.turn.handoff?.soap?.assessment);
}
Batch evaluation is two calls: batches.create(scenarios) then
batches.wait(batch_id) for the full typed transcripts. Webhook deliveries verify with one
function, verify_signature(body, signature, secret). Errors raise typed exceptions:
RateLimitedError carries retry_after, IntakeClosedError means stop
sending turns. The raw HTTP examples throughout these docs remain valid; the SDKs are the same contract
with the plumbing handled.
One key. One header.
Every request except GET /v1/health needs your API key in the
Authorization header. Sandbox keys start with adx_sb_.
Authorization: Bearer adx_sb_0123456789abcdef0123456789abcdef01234567
Keep the key on your server. Do not put it in a browser, a mobile app, or a repository. A request with a
missing, wrong, or revoked key returns 401 unauthorized. If your key leaks, email
team@auddax.ai
and we rotate it. Revocation takes effect within one minute.
Your key sees only its own intakes. A request for another key's encounter_id returns
404 not_found.
The full surface.
-
POST
/v1/intakes
Create an intake. Accepts a
labelandmetadatato group runs. -
GET
/v1/intakes
List your intakes, newest first. Filter by
labelandstatus. Free. - POST /v1/intakes/{encounter_id}/turns Send one patient message. Blocking or streaming.
-
GET
/v1/intakes/{encounter_id}
Status, outcome,
turn_in_progress, andlast_turn. Free. - GET /v1/intakes/{encounter_id}/result Compact result for assertions: outcome, routing, red flags, provenance.
- GET /v1/intakes/{encounter_id}/handoff Clinician handoff and transcript. Works after close and expiry.
- POST /v1/evaluate Beta. Evaluate a conversation you did not drive. Uses no turn quota. With no mode, this is the older deterministic path, not the consult engine.
- POST /v1/transcripts/assess Research read of a finished transcript. Off by default. Not for clinical use. See Transcript read.
- POST /v1/webhooks Register an event endpoint. Also GET to list, DELETE to remove.
- POST /v1/batches Run scripted scenarios as a background job. GET for status.
- GET /v1/config The configuration catalog: enabled modes, profiles, and flags. Free.
- GET /v1/usage Today's usage against your quotas. Free.
- GET /v1/usage/history Daily usage for up to 90 days. Free.
- GET /v1/key The calling key's metadata and limits. Free.
- GET /v1/health Liveness, release, and engine build. No auth.
-
GET
/v1/openapi.json
The machine-readable spec. Also
/v1/openapi.yaml. No auth. - GET /llms.txt A one-page guide for AI agents. No auth.
All request and response bodies are JSON in snake_case. The default request body limit is 64 KB. File
uploads accept up to 14 MB (a base64 file of at most 10 MiB), and POST /v1/evaluate and
POST /v1/transcripts/assess accept up to 1 MB. Every response carries an x-auddax-release header with the release tag and build.
Mode-specific routes (file uploads, autonomy reviews, and guided turns) are described in Engine functions and in the spec.
Create an intake · request fields
| Field | Type | Rules |
|---|---|---|
label |
string | Optional. 1 to 120 characters. A free-text tag for grouping runs, for example a scenario id. Never sent to the engine. Filter with GET /v1/intakes?label=. |
metadata |
object | Optional. Up to 20 string pairs. Keys are 1 to 40 letters, digits, or _ . : -. Values are strings of at most 200 characters. Never sent to the engine. |
demographics.age_years |
integer | Optional. 0 to 130. A patient under 18 is not eligible; see Engine behavior. |
demographics.sex |
string | Optional. One of female, male, intersex. |
patient_history |
string | Optional. Up to 16,000 characters after trim. Include the source, date, and review status of history entries. |
mode · profile · config · session_settings |
various | Optional. See Configuration. A mode that is not enabled returns 400 mode_unavailable. |
Every field is optional. Unknown fields are refused with 400 invalid_request, so a misspelled
field fails loudly instead of being ignored.
Send a turn · request fields
| Field | Type | Rules |
|---|---|---|
message |
string | Required. 1 to 8,000 characters. The patient's message, unmodified. |
stream |
boolean | Optional. Default false. True returns Server-Sent Events. See streaming. |
The defaults mirror Certuma Link US production.
The sandbox engine runs with the same behavior defaults as Certuma Link US production. The engine owns
this behavior, and it can change with the engine build. GET /v1/health names the engine lane
and its build in engine.lane and engine.wrapper_build_sha. Quote the build with
any finding.
| Default | What you see |
|---|---|
| One question per turn | Each assistant_message asks one question. |
| Multi-select red-flag screens | Some turns list several warning signs at once, with choice_mode: "multiple". Answer every choice that applies in one message, or say none apply. |
| Close on a done signal | The engine closes an interview only when the patient says it has nothing more to add. This lane has no turn cap. See Testing with an agent. |
| Emergency close | An emergency closes the intake at once, with advice to call emergency services. outcome is urgent. |
| Self-harm decline accepted once | If the patient declines the self-harm question, the engine accepts the decline and does not ask again. |
| Under 18 is not eligible | The intake closes with outcome: "not_eligible", terminal_reason: "not_eligible_under_18", and no clinician handoff. |
| Split intake | The patient interview and the clinician assessment are separate steps. The working diagnosis, differentials, and management options are written for the clinician, in the handoff's clinical_assessment. |
| US resources | Patient-facing text names US resources: 911 for emergencies and 988 for a mental health crisis. config.locale es-AR and pt-BR change the language, not those resources. |
Four outcomes
Every turn, intake read, result, handoff, and batch turn carries a normalized outcome. Branch
on it instead of combining terminal and urgent yourself.
| Outcome | Terminal | Meaning | Next |
|---|---|---|---|
continue | No | The intake is still asking questions. | Send the next patient message. |
complete | Yes | The intake closed with a clinician handoff. | Read the result or the handoff. |
urgent | Yes | The intake closed with an urgent escalation. | Surface it at once. Read the handoff. |
not_eligible | Yes | The intake closed without a handoff, for example a patient under 18. terminal_reason says why. | Stop. Start a new intake for a different patient. |
On the intake read and list, outcome is null before the first turn.
Open, then closed or expired.
An intake opens when you create it. It closes when the engine reaches a terminal state. It expires when its session reaches the two-hour limit before a terminal state. The handoff stays readable in every state.
| Status | Meaning | Turns | Handoff |
|---|---|---|---|
open | The intake accepts turns. | Accepted | Readable |
closed | The engine reached a terminal state. | 409 intake_closed | Readable |
expired | The session passed the two-hour limit. | 410 intake_expired | Readable |
Check the state at any time with GET /v1/intakes/{encounter_id}. It is free and never calls the
engine. The summary includes the label and metadata, the outcome, the
turn count, the expiry time, and the last known safety status and disposition. It also carries
turn_in_progress, true while a turn on this intake is producing its reply, and
last_turn, the last reply with its turn_index, assistant_message,
choices, and outcome. Read it to recover after a client timeout instead of
resending.
GET /v1/intakes lists your intakes, newest first, with the same summary fields except
last_turn. Filter with label and status (open,
closed, expired). limit takes 1 to 200 (default 50). To page, pass
before set to the previous page's next_before.
Every turn returns the clinical state.
A turn response always carries the same fields, terminal or not. The engine is authoritative. Show its message, honor its status, and do not soften its escalations.
| Field | Type | Meaning |
|---|---|---|
encounter_id | string | The intake this turn belongs to. |
turn_index | integer | The position of this turn, starting at 1. |
assistant_message | string | The next patient-facing message. Relay it without edits. |
choices | string[] | The engine answer choices. Preserve the full list. |
choice_mode | string | If multiple, the turn is a multi-select screen: collect all selected answers and send them together in one message. If absent, use the existing single-answer behavior. |
outcome | string | continue, complete, urgent, or not_eligible. Keep sending turns only while it is continue. See Engine behavior. |
terminal_reason | string | null | Why a terminal intake closed without a handoff, for example not_eligible_under_18. Null otherwise. |
should_continue_intake | boolean | The engine's own statement of whether the intake can continue, when it sends one. False closes the intake. |
terminal | boolean | True when the intake reached a terminal state. Stop sending turns. |
urgent | boolean | True when the engine requires immediate clinician review. Surface it prominently. |
safety_status | string | null | The deterministic safety state. Values include no_red_flags_yet, cannot_miss_negative, cannot_miss_positive. |
disposition | string | null | The engine's disposition. Values include continue_intake, clinician_review, self_care, urgent_escalation. |
disposition_label | string | null | A display label for the disposition. |
handoff | object | Present when terminal is true. The full handoff object. |
mode | string | The engine mode the intake runs in. See Engine functions. |
The safety fields can be null on a turn in one rare case: the turn completed but the snapshot read behind it
failed. The turn is still delivered. Read
GET /v1/intakes/{encounter_id}/handoff to get the state. Do not resend the turn.
Stream anything a patient can see.
A turn drives a real model turn inside the engine. The complete result takes 10 to 40 seconds. Streaming removes the wait a patient would feel. The reply arrives token by token within seconds, and your interface stays alive while the engine compiles the clinical snapshot behind it.
Set "stream": true on the turns endpoint. The response is
text/event-stream. Each event has an event: name and one
data: line of JSON.
$ curl -N -X POST https://api.auddax.ai/v1/intakes/$ENCOUNTER_ID/turns \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "It hurts when I swallow.", "stream": true }'
import json
with httpx.stream(
"POST",
f"{BASE}/v1/intakes/{encounter_id}/turns",
headers=HEADERS,
json={"message": "It hurts when I swallow.", "stream": True},
timeout=240,
) as stream:
event = None
for line in stream.iter_lines():
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event:
data = json.loads(line[6:])
if event == "message.delta":
print(data["text"], end="", flush=True)
elif event == "turn.completed":
turn = data
const res = await fetch(`${BASE}/v1/intakes/${encounterId}/turns`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ message: "It hurts when I swallow.", stream: true }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let turn = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const blocks = buffer.split(/\r?\n\r?\n/);
buffer = blocks.pop() || "";
for (const block of blocks) {
const event = /^event: (.+)$/m.exec(block)?.[1];
const data = JSON.parse(/^data: (.+)$/m.exec(block)?.[1] || "{}");
if (event === "message.delta") render(data.text);
if (event === "turn.completed") turn = data;
}
}
Events
| Event | Data | Meaning |
|---|---|---|
message.delta | { "text" } | The next fragment of the patient-facing reply. Append it. |
message.completed | { "elapsed_ms" } | The reply text is complete. You may send the next turn now. |
handoff.compiling | { "terminal" } | The engine compiles the clinical snapshot. Terminal tells you the intake is closing. |
handoff.progress | { "chars" } | Compile progress. Useful for a subtle activity indicator. |
turn.completed | turn object | The same object the blocking response returns, handoff included when terminal. |
error | { "code", "message" } | The turn failed after the stream started. Read the handoff before you retry anything. |
This is a recorded sandbox turn, replayed with its real timing compressed. Press play to see the event order.
Recorded sandbox events. Timing compressed for the demo.
message.completed arrives, the
patient can answer. Send the next turn immediately. The engine accepts it while the previous turn's snapshot
still compiles. This is verified behavior, not an accident.
message.delta text can differ from the final reply: the engine can remove a sentence after it
was streamed. The assistant_message in turn.completed is the authoritative text.
Replace the streamed text with it.
If a stream drops, do not resend the message blindly. The turn may have been delivered. Read
GET /v1/intakes/{encounter_id}: turn_in_progress and last_turn tell
you whether the message landed. Then continue.
Plan for the latency budget.
Each turn runs a governed clinical reasoning pass. It is slower than a chat completion, and it is supposed to be. Design for the budget instead of hiding from it.
| Milestone | Typical, warm | What to show |
|---|---|---|
First message.delta | A few seconds | The reply, appearing as it streams. |
| Complete patient message | 10 to 40 seconds | The full reply. The patient can answer now. |
| Snapshot and handoff | Shortly after | Nothing. It compiles in the background. |
Set your client timeout to at least 300 seconds. A blocking closing turn can take longer than the table
says, because the engine writes the note before it answers. The host api.auddax.ai runs
through a proxy. When a response has not started in 120 seconds, the proxy returns its own
502, and that body can be something other than the JSON error envelope, so read the status
before you parse it. The turn can still finish. The gateway behind the proxy holds a turn open for up to
285 seconds; if the engine has not answered by then, the gateway returns
504 upstream_timeout. If the proxy or your own client gives up first, retry with the same
Idempotency-Key (see idempotency and retries).
Use streaming for every patient-facing surface: text arrives within seconds, so a stream normally stays
inside the proxy limit. Use blocking calls for server-side and batch work where nobody watches a
spinner.
Get told when an intake ends.
Register an https endpoint and the API calls you when an intake reaches a terminal state. No polling. A webhook problem never fails or slows a turn.
$ curl -X POST https://api.auddax.ai/v1/webhooks \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/auddax/hook" }'
{
"id": "wh_9f2a51c07d3e44b1",
"url": "https://example.com/auddax/hook",
"events": ["intake.completed", "intake.escalated"],
"secret": "whsec_...shown once, store it...",
"created_at": "2026-08-21T18:20:11.312Z"
}
GET /v1/webhooks lists your endpoints (without secrets). DELETE /v1/webhooks/{id}
removes one. Each key holds up to 3 endpoints. The target must be a public https URL.
Events
| Event | Fires when |
|---|---|
intake.completed | An intake reaches a terminal state without an urgent escalation. |
intake.escalated | An intake ends with an urgent escalation. Treat it with priority. |
batch.completed | A batch run finishes. Arrives with the batch API. |
The delivery body is { id, event, created_at, data }. For intake events, data is
the same object the terminal turn returned, handoff included. Headers: auddax-event,
auddax-delivery, and auddax-signature.
Verify the signature
The signature header is t=<unix>,v1=<hex>. Compute HMAC-SHA256 over
t + "." + raw_body with your endpoint secret and compare. Reject stale timestamps.
const [, t, v1] = /t=(\d+),v1=([a-f0-9]+)/.exec(req.headers["auddax-signature"]);
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET)
.update(`${t}.${rawBody}`)
.digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
&& Math.abs(Date.now() / 1000 - Number(t)) < 300;
Answer with a 2xx within 10 seconds. Anything else retries: 3 attempts with backoff from 30 seconds to
5 minutes. Deliveries are at-least-once, so make your handler idempotent on the
auddax-delivery id.
Evaluate the engine with scripted scenarios.
A batch runs synthetic patient conversations for you in the background. Send up to 20 scenarios with up to 12 messages each. The engine drives every scenario as a real intake, stops a scenario early when it reaches a terminal state, and stores the transcript. Use batches for benchmarking and regression checks.
$ curl -X POST https://api.auddax.ai/v1/batches \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "regression-aug",
"scenarios": [
{ "label": "benign", "messages": ["Sore throat since yesterday.", "It hurts when I swallow."] },
{ "label": "urgent", "messages": ["Chest pressure for an hour, sweating."],
"demographics": { "age_years": 58, "sex": "male" } }
]
}'
{ "batch_id": "bat_5f0c33a1b2d4e6f8", "status": "queued", "scenario_count": 2 }
Poll GET /v1/batches/{batch_id} for status and per-scenario results, or register a webhook for
batch.completed and skip polling. Each scenario result carries its
encounter_id; fetch the full handoff with the normal handoff endpoint.
GET /v1/batches lists your recent batches.
Each scenario's turns pairs every patient_message with the engine's
assistant_message and the turn's outcome, so you can score a transcript without
another call. Each scenario's intake carries the scenario label (or the batch label) and the metadata
batch_id and scenario_index, so GET /v1/intakes?label= finds it.
{
"index": 1,
"status": "completed",
"label": "urgent",
"encounter_id": "enc_auddax-sandbox_5be0a7c19d2f4e83",
"turns": [
{
"turn_index": 1,
"patient_message": "Chest pressure for an hour, sweating.",
"assistant_message": "I need you to call 911 right now. [...]",
"terminal": true,
"urgent": true,
"outcome": "urgent",
"safety_status": "cannot_miss_positive",
"disposition": "urgent_escalation"
}
]
}
The rules that keep batches safe: batch turns draw from the same daily turn quota as
interactive calls, one batch runs at a time per key (a second create returns
409 batch_running), scenarios run with limited concurrency, and a quota-starved scenario is
marked failed with its partial transcript kept. Batch statuses: queued,
running, completed, completed_with_failures.
Select the engine mode
Read GET /v1/config for engine.modes. The operator configures the available connections. Existing requests use standard mode. A selected mode stays with the intake for every turn and handoff.
| Mode | Functions |
|---|---|
standard | The existing clinical intake and handoff. |
configured | Session settings for interview policies, prompt profile, clinician locale, and session length. Upload analysis uses the same intake. |
autonomous | The engine autonomy result, reasons, condition, and plan. The current engine contract uses en-US. |
Set mode when creating an intake. Supply session_settings only in configured mode. See Configuration for the accepted values and a request example. Unknown settings are refused.
Send a file to POST /v1/intakes/{encounter_id}/uploads as content_base64. Use PDF, JPEG, PNG, or WebP, at most 10 MiB. Check upload.status and the verification fields. The next turn reads the analysis. One analysis attempt uses one daily turn quota unit.
An autonomous handoff can contain autonomy. Use the engine's outcome, reasons, and plan. The model field is the proposal before the gate. The autonomy catalog is separate from the protocol reference corpus.
Read synthetic cases from GET /v1/autonomy/scenarios. Read or add reviews at /v1/intakes/{encounter_id}/autonomy/reviews. Review access requires the intake owner's API key. Uploads and review writes do not support automatic retries after a network or engine failure.
Evaluate a conversation you did not drive.
The intake endpoints drive the interview: the engine asks the questions and reaches a disposition.
Evaluate is the other shape. When a clinician runs the conversation and your product
only records and analyzes it, you do not want the engine asking questions. You want the part that has to
be right: catch the red flags and emergencies, evaluate the protocol, and decide the disposition over
whatever the conversation has surfaced so far. That is POST /v1/evaluate.
POST /v1/evaluate with no mode
is a separate older deterministic path. It asks no questions, runs no model, and returns no
clinical_assessment. Do not use its result to judge the interview. To test the interview, use
the intake endpoints.
The division of labor is the whole point. Your model does perception only: it turns the
live transcript into the typed facts the engine needs, or you simply hand over the transcript text.
MedCanon is the authority: it, not your model, sets every red-flag state, the emergency
findings, and the disposition. No model runs inside the evaluate path, so the result is deterministic:
the same input gives the same output, and every response is stamped provenance.deterministic: true.
The scribe loop
A scribe records a growing clinician–patient conversation. On each new batch of speech, and once more at the end of the consult, you send the current state to evaluate and render what it returns.
2.
POST /v1/evaluate with the facts and/or the transcript.3. The engine returns the emergency screen, coverage, the protocol result, and the disposition.
4. Render the alerts and the disposition. Draft the note separately; the note is never the source of clinical truth. The orders and red-flag states come only from evaluate.
Call it about every 15 seconds while the consult is live, and once over the final transcript. It is fast because it is deterministic: no model turn, typically well under a second. It has its own generous rate class (at least 120 requests per minute) and does not draw from the intake turn quota.
Request
Send transcript_text, facts, or both. transcript_text is the
fastest way to integrate and drives the emergency screen; facts (with a
protocol_id) gives the precise rule verdict.
$ curl -X POST https://api.auddax.ai/v1/evaluate \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"correlation_id": "consult-8f3a",
"input": {
"transcript_text": [
{ "speaker": "clinician", "text": "What brings you in today?" },
{ "speaker": "patient", "text": "The worst headache of my life, it hit like a thunderclap a minute ago." }
]
}
}'
import os, httpx
BASE = "https://api.auddax.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['AUDDAX_API_KEY']}"}
result = httpx.post(
f"{BASE}/v1/evaluate",
headers=HEADERS,
json={
"correlation_id": "consult-8f3a",
"input": {
"transcript_text": [
{"speaker": "clinician", "text": "What brings you in today?"},
{"speaker": "patient", "text": "The worst headache of my life, thunderclap onset a minute ago."},
],
# or send extracted facts for a precise verdict:
# "protocol_id": "headache",
# "facts": {"sudden_onset_thunderclap": True, "has_nausea_or_vomiting": True},
},
},
timeout=30,
).json()
if result["emergency"]["urgent"]:
escalate(result) # render the alert; the engine, not your model, decided this
const res = await fetch("https://api.auddax.ai/v1/evaluate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AUDDAX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
correlation_id: "consult-8f3a",
input: {
transcript_text: [
{ speaker: "clinician", text: "What brings you in today?" },
{ speaker: "patient", text: "The worst headache of my life, thunderclap onset a minute ago." },
],
},
}),
});
const result = await res.json();
if (result.emergency.urgent) escalate(result);
Request fields
| Field | Type | Rules |
|---|---|---|
correlation_id | string | Optional. For your own audit correlation only. The server keeps no state for it, so evaluate is fully stateless. |
input.transcript_text | array | The conversation so far, as { "speaker": "clinician" | "patient", "text": "..." }. Read by the deterministic emergency screen. No model reads it. |
input.facts | object | The extracted typed field values, as { field_id: value }. Deterministic protocol evaluation runs on these. |
input.protocol_id | string | Required when facts is sent. May be omitted with transcript_text; the engine detects the protocol. |
input.demographics | object | Optional. { "age": 47, "sex": "M" }. Feeds age- and sex-gated rules. |
Send at least one of transcript_text or facts. An empty input returns
400 invalid_request.
Response
{
"correlation_id": "consult-8f3a",
"coverage_state": "covered",
"active_protocol": { "id": "headache" },
"emergency": {
"urgent": true,
"findings": [
{ "id": "stroke_pattern", "provenance": "deterministic_emergency_canon" },
{ "id": "thunderclap_headache", "provenance": "deterministic_protocol" }
]
},
"protocol_result": {
"matched": true,
"rule_id": "red_flag_urgent_referral",
"diagnosis": { "label": "Urgent Referral Required", "disposition": "urgent" },
"derived_values": { "has_red_flags": true, "has_nausea_or_vomiting": true }
},
"disposition": "urgent",
"safety_status": "cannot_miss_positive",
"provenance": {
"deterministic": true,
"engine_release": "2026.11",
"audit_ref": "audit_f0812c423021"
}
}
| Field | What it means |
|---|---|
coverage_state | covered when a protocol applies to the conversation, else uncovered or none. |
active_protocol | The protocol the engine selected, or null. You did not choose it; the engine did. |
emergency | urgent plus the deterministic findings. Runs independent of protocol selection, so a cannot-miss emergency is caught even when no protocol matches. Each finding names its source. |
protocol_result | The rule that fired: matched, rule_id, diagnosis (with disposition), and the derived_values the rules computed. |
disposition | The engine's routing decision, e.g. urgent. |
safety_status | cannot_miss_positive, screening, or clear. Set by the engine; your model cannot set, clear, or delete it. |
provenance | deterministic is true whenever no model ran (the facts and transcript paths). Carries the engine release and an audit_ref you can quote when reporting an issue. |
cannot_miss, missing_fields, suggested_verifications | Coming. Three-valued red-flag states, the fields still worth extracting, and clinician prompts for unknown high-risk items. |
Determinism and safety
The shape is designed so the model cannot be the clinical authority, by construction:
| Guarantee | How |
|---|---|
| The model never decides | Red-flag states, emergency, and disposition come only from the engine. Your model fills in the blanks; it does not set the flags. |
| Emergencies are always screened | The emergency block runs even when no protocol is detected, so a heart attack or anaphylaxis is caught regardless of routing. |
| Deterministic and replayable | No model in the path. The same facts and transcript give the same result, and provenance.deterministic proves it. |
| Stateless and idempotent | Send the full current state each call. The server stores nothing per correlation_id; retries are safe. |
Extracting against typed fields (the facts path) needs the per-protocol field schema, which
tells your model exactly what to look for. A machine-readable schema endpoint is coming; until then, the
transcript_text path gives you the full emergency screen and protocol result with no schema
required. Reply to your onboarding contact with an audit_ref if any result looks wrong.
Read a finished transcript in one pass.
POST /v1/transcripts/assess forces a
diagnosis, which the live product does not force. It asks no questions, and it is not an intake. It is off
by default: your Auddax contact enables it per key or per account. Any other key gets
503 unavailable, and a retry does not change that. The sandbox terms apply.
$ curl -X POST $AUDDAX_DIRECT_BASE_URL/v1/transcripts/assess \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: chat-0001" \
--max-time 330 \
-d '{
"messages": [
{ "role": "patient", "content": "I have had a sore throat for two days." },
{ "role": "clinician", "content": "Do you have a fever?" },
{ "role": "patient", "content": "A low one, last night." }
],
"locale": "en-US",
"label": "run-1"
}'
{
"status": "final",
"not_eligible_reason": null,
"disposition": "clinician_review",
"disposition_label": "[...]",
"safety_status": "[...]",
"requires_immediate_clinician_review": false,
"clinical_assessment": {
"working_diagnosis": "[...]",
"differentials": ["[...]"],
"management": [{ "kind": "treatment_option", "text": "[...]" }]
},
"clinician_note": "[...]",
"active_protocol": "[...]",
"grounding": { "status": "[...]", "reason": null },
"dialogue_error": null,
"research_read": true,
"provenance": { "source_commit": "[...]", "prompt_profile": "[...]", "server_model": "[...]", "note_effort": "[...]" }
}
| Rule | What it means |
|---|---|
| Cut the transcript | You must cut each transcript before the first clinician message that gives a diagnosis or a plan, because the route cannot clean a transcript. A message after that point reaches the engine as input. |
| No doses | Treatment options carry no doses, by design, and the clinician decides. Nothing in management is decided. |
| Under 18 | A chat judged to be about a person under 18 returns status: "not_eligible" with no assessment. |
| Fields | Read clinical_assessment.working_diagnosis for the diagnosis and disposition for triage. research_read is always true. The gateway returns the engine's response unchanged. |
| Limits | A 1 MiB body, 1 to 600 messages with at least one from the patient, and 120,000 transcript characters. role is patient or clinician. locale is en-US, es-AR, or pt-BR. label is at most 120 characters. Unknown fields are refused. |
| Cost | One request of the per-minute limit and one unit of the daily turn quota. A request that the gateway refuses costs no quota. |
| Timeout | The gateway waits up to 300 seconds for the engine, then returns 504 upstream_timeout. Set your client timeout to at least 330 seconds. |
| Base URL | The proxy at api.auddax.ai returns its own 502 when a response has not started in 120 seconds, and a read can take longer. Call this route on the direct base URL your Auddax contact gives you. |
| No replay | The gateway stores no transcript text and no output, and it logs counts only. It keeps a hash of the request, so it cannot return a result twice. A required Idempotency-Key of up to 128 letters, digits, or _ . : - guards each run. |
| Retries | A used key gets 409 operation_conflict, also while the first request still runs. After a lost connection, 502, 503, or 504, the run may have happened and the result is gone: send a new key. That is a new model pass and a new quota unit. A key stays free after 429 and after a connection that could not open. |
Select the mode and session settings.
Set mode when you create an intake. Use config.locale for the patient
dialogue language. Use session_settings in configured mode for interview policies,
the prompt profile, the clinician language, and the session length. These settings stay with the intake.
Read the catalog
GET /v1/config returns two groups. flags lists the values accepted in
config. This group currently contains only locale.
engine.modes lists the enabled connections. When configured mode is enabled,
engine.session_settings lists its accepted settings.
$ curl https://api.auddax.ai/v1/config \
-H "Authorization: Bearer $AUDDAX_API_KEY"
Example response with all three connections enabled. Use the modes returned by your API deployment.
If engine is absent, that deployment does not expose this connection catalog.
{
"catalog_version": "2026-09-04",
"profiles": [ { "id": "default", "description": "Engine defaults." } ],
"flags": [
{ "key": "locale", "values": ["en-US", "es-AR", "pt-BR"], "default": "en-US",
"description": "Language of the patient-facing dialogue." }
],
"engine": {
"contract_version": "engine_connections_v1",
"modes": ["standard", "configured", "autonomous"],
"session_settings": {
"policies": {
"interview_pacing_policy": "interview_pacing_v1",
"question_budget_policy": "single_question_v1",
"answer_choices_policy": "multi_select_screen_v1"
},
"prompt_profiles": ["protocolos_studio_may11_v1", "protocolos_studio_plus10_v2"],
"clinician_locales": ["en-US", "es-AR", "pt-BR"],
"session_ttl_minutes": { "min": 5, "max": 1440 }
},
"uploads": {
"mode": "configured",
"max_bytes": 10485760,
"max_per_session": 10,
"mime_types": ["application/pdf", "image/jpeg", "image/png", "image/webp"]
}
}
}
Set the patient language
Send config when you create an intake. The response echoes the resolved config and its hash.
The same config applies to every turn in that encounter.
$ curl -X POST https://api.auddax.ai/v1/intakes \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "config": { "locale": "es-AR" } }'
{
"encounter_id": "enc_...",
"status": "open",
"config": { "locale": "es-AR" },
"config_hash": "722a56cd1465f74c"
}
| Patient writes in | config.locale | Clinician fields on auddax-sandbox |
|---|---|---|
| English | en-US (the default) | English |
| Spanish | es-AR | English |
| Portuguese | pt-BR | English |
Any other value is refused with 400 invalid_request, for example es-MX or
es. The engine writes to the patient in the locale, not in the language the patient writes in,
so with the default a patient who writes in Spanish gets replies in English. Set the locale when you create
the intake. The clinician fields are soap, clinician_summary, and
clinical_assessment. In pt-BR, the not-eligible close and the self-harm support
line are fixed text in English.
An omitted mode uses standard. Each batch scenario also accepts
config. Batches use standard mode and do not accept session_settings.
Evaluate accepts config through its separate request contract. Intake modes and
session_settings do not apply to evaluate.
Intake configuration fields
| Field | Values | Use |
|---|---|---|
mode | standard, configured, autonomous | Select an enabled connection. Default standard. The current autonomous contract requires en-US. |
config.locale | en-US, es-AR, pt-BR | The language of the patient dialogue. Default en-US. |
profile | default | Apply a named configuration bundle. Values in config override the bundle. This field is separate from the engine prompt profile. |
Settings for configured mode
Put these fields inside session_settings. Each field is optional. Put the three
policy fields inside session_settings.policies. Omitted settings keep the engine defaults.
| Field | Accepted value | Use |
|---|---|---|
policies.interview_pacing_policy | interview_pacing_v1 | Select the engine interview pacing policy. |
policies.question_budget_policy | single_question_v1 | Select the policy for one question per turn. |
policies.answer_choices_policy | multi_select_screen_v1 | Allow questions with multiple answer selections. Follow the returned choice_mode. |
prompt_profile_id | protocolos_studio_may11_v1, protocolos_studio_plus10_v2 | Select an engine prompt profile. |
clinician_locale | en-US, es-AR, pt-BR | Set the clinician output language. |
session_ttl_minutes | Integer from 5 to 1440 | Set the session length in minutes. |
Apply session settings
Use this request only when engine.modes includes configured.
The create response returns mode, config, the supplied
session_settings, and config_hash.
$ curl -X POST https://api.auddax.ai/v1/intakes \
-H "Authorization: Bearer $AUDDAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "configured",
"config": { "locale": "es-AR" },
"session_settings": {
"policies": {
"interview_pacing_policy": "interview_pacing_v1",
"question_budget_policy": "single_question_v1",
"answer_choices_policy": "multi_select_screen_v1"
},
"prompt_profile_id": "protocolos_studio_plus10_v2",
"clinician_locale": "es-AR",
"session_ttl_minutes": 720
}
}'
Both SDKs read the catalog with client.config(). For intake creation, the Python SDK
accepts mode and session_settings. The TypeScript SDK accepts
mode and sessionSettings. The API uses session_settings.
The configuration hash identifies the requested settings. It does not identify the engine build or guarantee the same clinical result. Read the handoff provenance for the engine version. See Engine functions for uploads and autonomy.
session_settings
in standard or autonomous mode. A mode that is not enabled on this deployment returns
400 mode_unavailable, and its message lists the enabled modes.
A delivered turn is never re-driven.
Turns are long calls, so client timeouts happen. A naive retry would drive a second clinical turn with the
same message. The Idempotency-Key header prevents that.
| Call | Safe to retry | Rule |
|---|---|---|
POST /v1/intakes |
Yes | A duplicate creates a second empty intake. Use the newest encounter_id. |
Blocking turn with Idempotency-Key |
Yes | The key is claimed before the turn runs. A repeated key returns the stored response and consumes no quota; the replay carries the header x-auddax-idempotent-replay: true. While the first request still runs, a retry gets 409 turn_in_progress with Retry-After. A turn that fails with any error except 504 frees its key, so the same key can retry it. |
| Blocking turn without a key | No | A retry drives a second turn. Always send the header. |
| Streaming turn | No | Idempotency does not apply to streams. If a stream drops, read GET /v1/intakes/{encounter_id} first. |
POST /v1/transcripts/assess |
No replay | The route stores no result, so a used key gets 409 operation_conflict. A new key runs a new model pass. See Transcript read. |
GET requests |
Yes | All reads are safe to repeat. |
One turn per intake at a time. A turn sent while another turn on the same intake is still
producing its reply gets 409 turn_in_progress, and nothing is sent to the engine. With streaming,
the intake accepts the next turn once message.completed arrives.
Recover after a client timeout or a 502 from the proxy. Retry the same request with the same key, or read
GET /v1/intakes/{encounter_id}. It is free: turn_in_progress says whether a turn
is still running, and last_turn holds the last reply. Poll it while you wait instead of
resending; retries count against the per-minute limit.
504 upstream_timeout is ambiguous. The engine did not answer in time, and the
turn may still complete. The gateway keeps the intake locked for a while, and it refuses that key from then
on: a retry with it gets 409 turn_outcome_unknown, so the message can never reach the engine
twice. Wait until GET /v1/intakes/{encounter_id} shows turn_in_progress: false,
then read GET /v1/intakes/{encounter_id}/handoff. If your message is the last patient message
in conversation, the engine has it and the reply follows it. If not, send the message again
with a new key.
Choose one key per patient message, for example a UUID or turn-3-enc_abc. Never reuse a key
for a different message, and never send a new key for a message you already sent. Replayed responses stay
available for about 24 hours.
Stable codes. Clear next steps.
Every error uses one envelope. Branch on error.code, not on the message text.
409 turn_in_progress, 429, and 504 carry a Retry-After
header in seconds.
{
"error": {
"code": "intake_closed",
"message": "The intake reached a terminal state and takes no more turns.",
"encounter_id": "enc_auddax-sandbox_91a81eb05e4c58fa"
}
}
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request | Bad JSON, a missing or unknown field, an out-of-range value, or a body over the endpoint limit. | Fix the request. The message names the field. Do not retry as-is. |
| 400 | mode_unavailable | The requested mode is not enabled on this deployment. | Use a mode from engine.modes in GET /v1/config, or omit mode for standard. Do not retry as-is. |
| 401 | unauthorized | The key is missing, unknown, or revoked. | Check the header and the key. |
| 404 | not_found | The intake does not exist for this key, or the path is unknown. | Check the encounter_id and the path. |
| 409 | intake_closed | The intake reached a terminal state. | Stop sending turns. Read the result or the handoff. |
| 409 | turn_outcome_unknown | An earlier request with this Idempotency-Key timed out, and the engine may have the message. | Do not retry the key. Read the handoff conversation; if your message is not there, send it with a new key. |
| 409 | turn_in_progress | A turn on this intake is still producing its reply, or a request with this Idempotency-Key is still running. Nothing new was sent to the engine. | Wait Retry-After seconds. Poll GET /v1/intakes/{encounter_id} (free) for turn_in_progress and last_turn, then retry with the same key. |
| 409 | operation_conflict | The request conflicts with stored state: a guided turn_id or an evaluate Idempotency-Key reused with different input, a used key on POST /v1/transcripts/assess, or an upload or review the session is not ready for. | Reload the current state. Send new input with a new key or turn_id. Do not retry as-is. |
| 409 | batch_running | A batch is already running for this key. | Wait for it, or poll its status. |
| 410 | intake_expired | The session passed the two-hour limit. | Create a new intake. The handoff stays readable. |
| 429 | rate_limited | Too many requests this minute. | Wait for Retry-After seconds. |
| 429 | quota_exceeded | The daily turn or intake quota is used up. | Stop. The quota resets at 00:00 UTC; Retry-After counts the seconds until then. Ask us to raise the quota if you need more. |
| 502 | upstream_error | The engine failed on this call. | Retry later with the same Idempotency-Key. A failed turn frees its key. |
| 502 | None | The proxy at api.auddax.ai did not get a response in 120 seconds. This body can be something other than this envelope. The turn can still finish. | Read the status before you parse the body. Retry a blocking turn with the same Idempotency-Key. |
| 503 | unavailable | The engine is unreachable, or a feature is switched off. | Retry with backoff, with the same Idempotency-Key. A switched-off feature says so in the message, and a retry does not change it. |
| 504 | upstream_timeout | The engine did not answer in time. The turn may still complete. | Do not retry the key. Wait until turn_in_progress is false, then read the handoff conversation. If your message is not there, send it with a new key. |
| 500 | internal_error | A gateway fault. | Retry once. Report it if it repeats. |
Sandbox limits are per key.
| Limit | Default | On breach |
|---|---|---|
| Requests per minute | 6 | 429 rate_limited with Retry-After |
| Turns per day | 100 | 429 quota_exceeded with Retry-After to 00:00 UTC |
| Intakes per day | 100 | 429 quota_exceeded with Retry-After to 00:00 UTC |
Every turn runs real clinical inference, so the sandbox caps spend per key. The daily window resets at 00:00 UTC. Need more for a serious evaluation? Email team@auddax.ai and we raise your quota.
Some reads are free: they do not count against the per-minute limit. They are
GET /v1/intakes, GET /v1/intakes/{encounter_id}, GET /v1/usage,
GET /v1/usage/history, GET /v1/config, GET /v1/key, and
GET /v1/health. The result and handoff reads, creates, turns, and turn retries that get
409 turn_in_progress do count. A replayed turn is free. One
POST /v1/transcripts/assess uses one request of the per-minute limit and one turn of the daily
quota.
Check where you stand at any time. Every turn response also carries an
x-auddax-turns-remaining header, so your client can watch the budget without an extra call.
{
"date": "2026-11-03",
"turns": { "used": 12, "quota": 100, "remaining": 88 },
"intakes": { "used": 3, "quota": 100, "remaining": 97 },
"evaluations": { "used": 0 },
"rpm_limit": 6
}
The clinician-ready result.
The handoff is the structured output of the intake. It arrives inside a terminal turn and from
GET /v1/intakes/{encounter_id}/handoff at any time.
{
"encounter_id": "enc_auddax-sandbox_91a81eb05e4c58fa",
"captured_at": "2026-08-21T16:32:41.902Z",
"terminal": true,
"urgent": true,
"outcome": "urgent",
"terminal_reason": null,
"intake_status": "complete",
"safety_status": "cannot_miss_positive",
"safety_notes": ["Cannot-miss pathway triggered."],
"disposition": "urgent_escalation",
"disposition_label": "Emergency evaluation",
"soap": {
"subjective": "58-year-old male with chief complaint of chest pressure...",
"objective": "No examination performed. Intake interview only.",
"assessment": "Presentation concerning for acute coronary syndrome...",
"plan": "Immediate emergency evaluation. 911 activation advised..."
},
"captured_facts": { "age": "58", "sex": "male", "pregnancy": null },
"active_protocol": "CERT-ACS-061",
"candidate_protocols": ["CERT-ACS-061", "CERT-CARD-060"],
"red_flags": ["Chest pressure at rest"],
"protocol_result": {
"detected_protocol": "CERT-ACS-061",
"coverage_state": "covered",
"safety_triage": "urgent",
"rule_id": "red_flag_urgent_referral",
"cannot_miss_screen_incomplete": false
},
"clinician_summary": "58-year-old male with one hour of chest pressure at rest...",
"clinical_assessment": null,
"allergies": "Penicillin",
"current_medication": "Lisinopril",
"medication_identity": { "unverified": [], "verified": ["lisinopril"] },
"references": null,
"scores": [
{ "key": "rfs", "total": "6/6", "band": "strong", "components": "2/2/2" }
],
"conversation": [
{ "role": "patient", "text": "Chest pressure for the last hour..." },
{ "role": "assistant", "text": "I need you to call 911 right now..." }
],
"provenance": {
"release": "release_staging_runtime_v1",
"server_model": "claude-sonnet-4-5",
"prompt_hash": "sha256:1f8c...",
"source_commit": "b9c502d1...",
"directory_hash": "sha256:77aa...",
"runtime_profile": "pure_llm_studio_v1",
"prompt_profile": "protocolos_studio_may11_v1",
"voice_profile": "warm_concise_clinician_led_v1",
"audit_ref": "req_a7393c1094d7",
"latency_ms": 21408,
"protocol_count": 151
}
}
| Field | Meaning |
|---|---|
terminal · urgent | The intake's end state. Urgent means immediate clinician review. |
outcome · terminal_reason | The normalized outcome (continue, complete, urgent, not_eligible) and, for a close without a handoff, the reason. See Engine behavior. |
intake_status | What the intake still needs, or complete. |
safety_status · safety_notes | The deterministic safety state and its notes. |
disposition · disposition_label | The engine's disposition and its display label. |
soap | The clinician summary: subjective, objective, assessment, plan. |
captured_facts | Structured demographics captured during the intake. |
active_protocol · candidate_protocols | The committed protocol and the differential considered. |
red_flags | The red flags the engine recorded, in its own words. |
protocol_result | The protocol verdict: detected_protocol, coverage_state, safety_triage, rule_id, and cannot_miss_screen_incomplete. The last is true when the intake closed with a cannot-miss screen still pending: a review signal, never a routing input. Null when the engine sends none. |
clinician_summary | The engine's clinician handoff summary text. It can open with a medication identity notice. |
clinical_assessment | The split intake's working_diagnosis, differentials, and management options, for the clinician. Null when the engine did not write one, for example on an open intake, a not-eligible close, or a failed note. working_diagnosis can read “Undetermined” with the reason. Treatment options carry no doses, by design, and the clinician decides. |
allergies · current_medication | What the intake captured, as text. Null when not captured. |
medication_identity | Generic medicine names in the note that the patient did not supply (unverified) or that the reference table resolved (verified). A review signal, not identity verification. |
references | Evidence references for the protocols the engine used, when the engine supplies them. Otherwise null. |
scores | Quality scores. Populated on terminal intakes. Bands are strong, mid, weak. |
conversation | The patient-facing dialogue. Clinician-only content never appears here. |
provenance | The attested run identity. See provenance. |
Which field to read
| You want | Read | Not this |
|---|---|---|
| Triage | disposition: clinician_review, self_care, urgent_escalation, or not_eligible, and continue_intake while the intake is open. disposition_label is its display text. | outcome says how the intake ended. safety_status is the state of the cannot-miss screen. safety_triage is only urgent or routine. |
| Diagnosis | clinical_assessment.working_diagnosis, with differentials and management. On the result, the handoff, and the handoff in a terminal turn. | active_protocol and detected_protocol name a protocol. soap.assessment is prose for the note. POST /v1/evaluate is a different path. |
The compact result
GET /v1/intakes/{encounter_id}/result returns one flat object a test harness can assert on. It
reads the handoff once, so it counts against the per-minute limit.
{
"encounter_id": "enc_auddax-sandbox_91a81eb05e4c58fa",
"label": "chest-pain-01",
"metadata": { "arm": "default", "run": "3" },
"status": "closed",
"turn_count": 1,
"outcome": "urgent",
"terminal_reason": null,
"safety_status": "cannot_miss_positive",
"disposition": "urgent_escalation",
"active_protocol": "CERT-ACS-061",
"safety_triage": "urgent",
"cannot_miss_screen_incomplete": false,
"red_flags": ["Chest pressure at rest"],
"clinical_assessment": {
"working_diagnosis": "[...]",
"differentials": ["[...]"],
"management": [{ "kind": "referral", "text": "[...]" }]
},
"medication_identity_unverified": [],
"patient_turns": 1,
"last_assistant_message": "I need you to call 911 right now...",
"provenance": { "release": "release_staging_runtime_v1", "source_commit": "b9c502d1...", "audit_ref": "req_a7393c1094d7" }
}
It also carries mode, config_hash, terminal, urgent,
disposition_label, candidate_protocols, coverage_state,
safety_notes, and the provenance server_model, prompt_hash, and
prompt_profile.
Attested, not asserted.
Every handoff carries the identity of the run that produced it, reported by the engine itself. Pin these values in your evaluation records. Compare them across releases.
| Field | Meaning |
|---|---|
server_model | The model the engine actually ran. |
release · source_commit | The engine release and its source commit. |
prompt_hash · directory_hash | Content hashes of the prompt and the pinned protocol library. |
runtime_profile · prompt_profile · voice_profile | The pinned engine configuration for this tenant. |
audit_ref | The engine's audit reference for this run. Quote it in any support request. |
protocol_count | The size of the pinned protocol library. |
GET /v1/health adds the serving side: engine.lane names the engine lane behind
standard mode, and engine.wrapper_build_sha names its build (null when unreadable). Record both
with source_commit when you compare runs.
A research preview. Not for care.
Send no real patient data. Do not send personal health information, names, contact details, or record numbers. Use synthetic scenarios. Test messages persist in the engine's encounter store and inform engine evaluation.
The sandbox moves. It runs against the staging engine. Behavior can change and encounters can reset without notice. Provenance fields tell you exactly which release served each response.
Keys are personal. One key per developer or service. Do not share keys. We revoke on request, and revocation lands within one minute.
Support and incidents:
team@auddax.ai.
Include the encounter_id and the audit_ref when you have them.
Machine-readable, versioned.
The OpenAPI 3.1 spec is served by the API itself at /v1/openapi.json and /v1/openapi.yaml. Generate clients from it, or load it into your API tooling. AI agents can start from the one-page guide at /llms.txt. None of these need a key.
The current release is 2026.11. Every response carries the release in a header.
GET /v1/health returns the same identity as JSON, plus the engine lane and build.
$ curl -sI https://api.auddax.ai/v1/health | grep x-auddax-release
x-auddax-release: 2026.11+bd1ee4f
$ curl -s https://api.auddax.ai/v1/health
{"ok":true,"release":"2026.11","build_sha":"bd1ee4f0f2d33c36db815e607cb51ee26a53df44",
"engine":{"lane":"auddax-staging","wrapper_build_sha":"0c4e9a7d2b1f..."}}
Typed clients are described in SDKs and event delivery in webhooks. For anything else, email team@auddax.ai.