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
terminal is true.
From key to handoff in three calls.
You need an API key. Request one by email and we send it over a secure channel. 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 '{
"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={
"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({
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",
"created_at": "2026-08-21T16:31:04.118Z",
"expires_at": "2026-08-21T18:31:04.000Z"
}
Send each patient message as one turn. 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,
"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, terminal stays false,
and choices can carry quick replies you can render as buttons.
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["soap"]["assessment"])
const handoff = await fetch(`${BASE}/v1/intakes/${encounterId}/handoff`, {
headers: HEADERS,
}).then((r) => r.json());
console.log(handoff.soap.assessment);
The full handoff shape is documented in the handoff object.
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
- POST /v1/intakes/{encounter_id}/turns Send one patient message. Blocking or streaming.
- GET /v1/intakes/{encounter_id} Status summary
- GET /v1/intakes/{encounter_id}/handoff Clinician handoff. Works after close and expiry.
- GET /v1/health Liveness and release identity. No auth.
- GET /v1/openapi.yaml The machine-readable spec
All request and response bodies are JSON in snake_case. Request bodies have a 64 KB limit. Every response
carries an x-auddax-release header with the release tag and build.
Create an intake · request fields
| Field | Type | Rules |
|---|---|---|
demographics.age_years |
integer | Optional. 0 to 130. |
demographics.sex |
string | Optional. One of female, male, intersex. |
patient_history |
string | Optional. Up to 4,000 characters. Free-text context such as allergies and current medications. |
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. |
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}. The summary includes the turn
count, the expiry time, and the last known safety status and disposition.
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[] | Engine-suggested quick replies. Render them as buttons if you want. |
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. |
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 20 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.
If a stream drops, do not resend the message blindly. The turn may have been delivered. Read the handoff to see the current state, 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 | 20 to 40 seconds | The full reply. The patient can answer now. |
| Snapshot and handoff | Shortly after | Nothing. It compiles in the background. |
The gateway holds a turn open for up to 240 seconds before it fails the call. Set your client timeout at 240 seconds or higher. Use streaming for every patient-facing surface. Use blocking calls for server-side and batch work where nobody watches a spinner.
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 | A repeated key returns the stored response and consumes no quota. The replay carries the header x-auddax-idempotent-replay: true. |
| 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 the handoff first. |
GET requests |
Yes | All reads are safe to repeat. |
Choose one key per logical turn, for example turn-3-enc_abc. 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.
{
"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 field, or an out-of-range value. | Fix the request. 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. | Check the encounter_id. |
| 409 | intake_closed | The intake reached a terminal state. | Stop sending turns. Read the handoff. |
| 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 quota is used up. | Resume tomorrow, or ask us to raise the quota. |
| 502 | upstream_error | The engine failed on this call. | Retry once with the same Idempotency-Key. |
| 503 | unavailable | The engine is unreachable. | Retry with backoff. |
| 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 |
| Intakes per day | 100 | 429 quota_exceeded |
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.
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,
"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"],
"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. |
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. |
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. |
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. |
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.yaml. Generate clients from it, or load it into your API tooling.
Every response carries the release in a header. GET /v1/health returns the same identity as
JSON.
$ curl -sI https://api.auddax.ai/v1/health | grep x-auddax-release
x-auddax-release: 2026.08+bd1ee4f
$ curl -s https://api.auddax.ai/v1/health
{"ok":true,"release":"2026.08","build_sha":"bd1ee4f0f2d33c36db815e607cb51ee26a53df44"}
Typed SDKs and webhooks are planned and not yet available. A reference client is available on request. For anything else, email team@auddax.ai.