Docs

Quickstart

Create a user, open a chat, and stream your first assistant message in staging.

Stream an assistant message from the Doctronic API in staging, using a synthetic patient. You need a staging API token; get access if you don't have one.

Set your environment. Run every request from a terminal or your backend, and keep the token there. Each step also shows the call with the TypeScript and Python SDKs. They read DOCTRONIC_API_TOKEN and target staging unless DOCTRONIC_ENVIRONMENT is production; DOCTRONIC_API_URL is only for curl and the raw HTTP examples.

export DOCTRONIC_API_URL="https://staging-partners.doctronic.ai/api/v1"
export DOCTRONIC_API_TOKEN="replace-with-your-staging-token"

1. Create a user

Create the Doctronic user for the patient with POST /users/.

# Reuse the same key and body if you retry this request.
IDEMPOTENCY_KEY="$(uuidgen)"
# Email and phone number are unique per user, so each run uses a fresh email and no phone number.
EMAIL="avery+quickstart-$(uuidgen)@example.com"

curl --fail-with-body -X POST "$DOCTRONIC_API_URL/users/" \
  -H "Authorization: Bearer $DOCTRONIC_API_TOKEN" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firstName": "Avery", "lastName": "Chen", "dateOfBirth": "1990-06-15",
       "email": "'"$EMAIL"'", "sex": "female"}'
{
  "code": "success",
  "message": "Request successful",
  "data": {
    "id": "mem_01K6JGGM2G2DB4Y9KB9H9MJJ6Q",
    "firstName": "Avery",
    "appointmentDemographicsComplete": false,
    "missingAppointmentFields": ["phoneNumber"]
  }
}

The response also echoes the other fields you sent. Save data.id (user.id in the SDKs, which return data itself):

export DOCTRONIC_USER_ID="mem_01K6JGGM2G2DB4Y9KB9H9MJJ6Q"

The Idempotency-Key makes a timed-out create safe to retry for 24 hours: resend the same body with the same key. See idempotent requests.

2. Create a chat

Open a chat for the consultation with POST /chats/. From here on, every request also sends X-Doctronic-User-ID.

# Reuse the same key if you retry this request.
IDEMPOTENCY_KEY="$(uuidgen)"

curl --fail-with-body -X POST "$DOCTRONIC_API_URL/chats/" \
  -H "Authorization: Bearer $DOCTRONIC_API_TOKEN" \
  -H "X-Doctronic-User-ID: $DOCTRONIC_USER_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timezone": "America/New_York"}'
{
  "code": "success",
  "message": "Request successful",
  "data": { "id": "chat_01K6JGGM2G2DB4Y9KB9H9MJJ6Q", "createdAt": "2026-08-31T14:31:00Z" }
}
export DOCTRONIC_CHAT_ID="chat_01K6JGGM2G2DB4Y9KB9H9MJJ6Q"

3. Stream a patient turn

Send the patient's message to POST /chats/{chatId}/stream/ and keep the connection open.

curl --fail-with-body --no-buffer -X POST "$DOCTRONIC_API_URL/chats/$DOCTRONIC_CHAT_ID/stream/" \
  -H "Authorization: Bearer $DOCTRONIC_API_TOKEN" \
  -H "X-Doctronic-User-ID: $DOCTRONIC_USER_ID" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"userEvent": {"type": "user_message",
       "userInput": "I have had a sore throat and fever since yesterday."},
       "timezone": "America/New_York"}'
event: message.start
data: {"type":"message.start","role":"assistant","messageId":"ac_hist_01K6JGHM5S4W7T9Y2B3C4D5E6F"}

event: message.content
data: {"type":"message.content","messageId":"ac_hist_01K6JGHM5S4W7T9Y2B3C4D5E6F","content":"I can help with that."}

event: message.stop
data: {"type":"message.stop","messageId":"ac_hist_01K6JGHM5S4W7T9Y2B3C4D5E6F"}

That's your first streamed message. To render it, take the role from message.start and append each message.content chunk to the message with the same messageId. message.stop ends that one message; the consultation continues until its end events arrive. Send each later patient turn to the same endpoint.

The stream turn is not idempotent. Do not automatically replay a turn after partial output; see stream errors.

Handle guardian events before patients see this

A turn can emit a guardian event that requires your product to act, whatever the assistant text says. Read Safety before you show the stream to patients.

4. Retrieve a consultation document

When the consultation completes, the stream emits ai_consultation.complete with artifact IDs:

event: ai_consultation.complete
data: {"type":"ai_consultation.complete","artifacts":{"longSummary":"art_01K6JGGM2G2DB4Y9KB9H9MJJ6Q","shortSummary":"art_01K6JGGP7Q4R8S2T5V7W9X1Y3","soapNote":"art_01K6JGGR9Z6B2C4D6F8H0J2K4M"}}

Fetch one with GET /artifacts/{artifactId}/:

curl --fail-with-body "$DOCTRONIC_API_URL/artifacts/art_01K6JGGP7Q4R8S2T5V7W9X1Y3/" \
  -H "Authorization: Bearer $DOCTRONIC_API_TOKEN" \
  -H "X-Doctronic-User-ID: $DOCTRONIC_USER_ID"
{
  "code": "success",
  "message": "Request successful",
  "data": {
    "artifactType": "short_summary",
    "artifactId": "art_01K6JGGP7Q4R8S2T5V7W9X1Y3",
    "content": { "chatId": "chat_01K6JGGM2G2DB4Y9KB9H9MJJ6Q", "body": "## Summary\n\nSymptoms began yesterday." }
  }
}

A 202 means the document is still generating; retry with backoff. See Consultation documents for every status.

Run it as a script

These programs run steps 1–3 and print each finished message and every other event. The SDK versions read the stream with chats.stream(), which parses it into typed events. The fetch and httpx versions use a deliberately minimal SSE reader; without an SDK, use the production parser in your backend.

// Node 22+. Run with: npx tsx quickstart.mts
const base = process.env.DOCTRONIC_API_URL ?? 'https://staging-partners.doctronic.ai/api/v1';

async function post(path: string, body: unknown, headers: Record<string, string> = {}) {
  const res = await fetch(`${base}${path}`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.DOCTRONIC_API_TOKEN}`, 'Content-Type': 'application/json', ...headers },
    body: JSON.stringify(body),
  });
  if (!res.ok || !res.body) throw new Error(`${path}: ${res.status} ${await res.text()}`);
  return res;
}

// Email and phone number are unique per user, so each run uses a fresh email and no phone number.
const patient = {
  firstName: 'Avery', lastName: 'Chen', dateOfBirth: '1990-06-15',
  email: `avery+quickstart-${crypto.randomUUID()}@example.com`, sex: 'female',
};
const user = await (await post('/users/', patient, { 'Idempotency-Key': crypto.randomUUID() })).json();
const scope = { 'X-Doctronic-User-ID': user.data.id };
const chat = await (
  await post('/chats/', { timezone: 'America/New_York' }, { ...scope, 'Idempotency-Key': crypto.randomUUID() })
).json();

const stream = await post(`/chats/${chat.data.id}/stream/`, {
  userEvent: { type: 'user_message', userInput: 'I have had a sore throat and fever since yesterday.' },
  timezone: 'America/New_York',
}, { ...scope, Accept: 'text/event-stream' });

const messages = new Map<string, { role: string; text: string }>();
const decoder = new TextDecoder();
let buffer = '';
let event = '';
let data = '';
for await (const chunk of stream.body!) {
  buffer += decoder.decode(chunk, { stream: true });
  const lines = buffer.split('\n');
  buffer = lines.pop() ?? '';
  for (const line of lines.map((l) => l.replace(/\r$/, ''))) {
    if (line.startsWith('event:')) event = line.slice(6).trim();
    else if (line.startsWith('data:')) data += line.slice(5).trim();
    else if (line === '' && data) {
      const payload = JSON.parse(data);
      if (event === 'message.start') messages.set(payload.messageId, { role: payload.role, text: '' });
      else if (event === 'message.content') messages.get(payload.messageId)!.text += payload.content;
      else if (event === 'message.stop') console.log(messages.get(payload.messageId));
      else console.log(event, payload); // guardian, conversation.end, error, and any new event
      event = '';
      data = '';
    }
  }
}

What to build next

On this page