Docs

Errors

Read API errors, retry mutations safely with idempotency keys, and recover from stream errors and timeouts.

Every JSON error has a stable machine-readable code and a human-readable message. Branch on code and the HTTP status; show or log message.

curl --include "$DOCTRONIC_API_URL/users/" \
  -H "Authorization: Bearer invalid-token"
{
  "code": "error.auth.unauthorized",
  "message": "Unauthorized."
}

Field validation failures return 400 with code set to error.validation_error and data set to a list of {field, message, code} entries. field is the request location and camelCase field path, separated by dots:

{
  "code": "error.validation_error",
  "message": "Validation error",
  "data": [
    {
      "field": "body.requiredField",
      "message": "Field required",
      "code": "error.validation.missing"
    }
  ]
}

HTTP status codes

StatusMeaningWhat to do
400The request is invalid, failed validation, or is missing X-Doctronic-User-ID.Correct it using code and, when present, data. Retrying it unchanged returns the same error.
401The API token is missing or invalid.Stop using the credential. See authentication.
403The API token lacks the scope the operation requires. data.requiredScope names it.Stop. Retrying with the same token returns the same error. See scopes.
404The resource is absent from the authenticated organization and user scope, or the user in X-Doctronic-User-ID isn't one of your organization's active users.Treat it as unavailable. Resources and users outside your scope return 404 too, so a 404 never reveals whether they exist elsewhere.
409A state conflict or an idempotency conflict.Read code to decide whether to refresh state or wait and retry.
422A legacy schema response kept in the contract for generated-client compatibility.Handle it like 400 if you ever receive it. The API returns 400 for validation failures.
429The API itself has no per-caller request limit and doesn't return 429.If a proxy or gateway in your path returns it, respect Retry-After when present, otherwise back off. Model capacity limits arrive as a stream error.
502An appointment couldn't be provisioned with the clinical service.Read code. For error.appointments.waiting_room_unavailable, the appointment exists: retrieve it before retrying.
503The operation couldn't start safely, or the service is briefly unavailable.Wait for Retry-After when present, then retry the same request.
Other 5xxService failure.Retry safe reads with bounded exponential backoff and jitter.

The SDKs throw one error class per status, the same in TypeScript and Python. Each extends APIError, which carries status, code, message, data, and the request ID.

StatusSDK error class
400, and the legacy 422BadRequestError
401AuthenticationError
403PermissionDeniedError
404NotFoundError
409ConflictError
429RateLimitError
503ServiceUnavailableError
Other 5xx, including 502InternalServerError
No responseAPIConnectionError, or APITimeoutError after the timeout

Some operations add their own statuses. GET /artifacts/{artifactId}/ returns 202 while a consultation document is still generating and 500 when generation failed permanently; see consultation documents.

Error code reference

Every error.* code in the OpenAPI contract. Codes are case-sensitive wire values.

CodeStatusReturned byMeaning
error.address.not_found404GET /address/The user has no address on file.
error.appointments.cannot_be_cancelled_or_rescheduled409POST /appointments/{appointment_id}/cancel/, POST /appointments/{appointment_id}/reschedule/The appointment's current state doesn't allow this change.
error.appointments.chat_not_found404POST /appointments/scheduled/, POST /appointments/asap/The chat doesn't exist or doesn't belong to this user.
error.appointments.chat_not_ready400POST /appointments/scheduled/, POST /appointments/asap/The chat isn't ready for appointment booking.
error.appointments.incomplete_address400GET /appointments/scheduled/available-slots/, POST /appointments/scheduled/, POST /appointments/asap/The user's address on file is incomplete.
error.appointments.invalid_slot400POST /appointments/scheduled/startsAt isn't a slot returned by GET /appointments/scheduled/available-slots/.
error.appointments.missing_address400GET /appointments/scheduled/available-slots/, POST /appointments/scheduled/, POST /appointments/asap/The user has no address on file.
error.appointments.missing_patient_demographics400POST /appointments/scheduled/, POST /appointments/asap/Required patient demographics are missing from the user record.
error.appointments.no_practitioners_available400POST /appointments/scheduled/, POST /appointments/asap/, POST /appointments/{appointment_id}/reschedule/No practitioners are available in the requested state.
error.appointments.not_found404GET /appointments/, GET /appointments/{appointment_id}/, GET /appointments/{appointment_id}/result/, POST /appointments/{appointment_id}/cancel/, POST /appointments/{appointment_id}/reschedule/The appointment doesn't exist in this scope.
error.appointments.patient_not_found404POST /appointments/{appointment_id}/cancel/, POST /appointments/{appointment_id}/reschedule/The appointment's patient record wasn't found.
error.appointments.provisioning_failed502POST /appointments/scheduled/, POST /appointments/asap/The clinical service didn't accept the booking.
error.appointments.result_unavailable409GET /appointments/{appointment_id}/result/The appointment has no available result.
error.appointments.slot_unavailable409POST /appointments/scheduled/The selected slot is no longer available. Refresh availability.
error.appointments.unavailable_time409POST /appointments/{appointment_id}/reschedule/The requested time slot is unavailable.
error.appointments.waiting_room_unavailable502POST /appointments/asap/The appointment is booked, but a secure waiting-room URL is unavailable. Retrieve the appointment before retrying.
error.artifact.not_found404GET /artifacts/{artifactId}/The artifact doesn't exist, or doesn't belong to this organization or user.
error.auth.insufficient_scope403Any operationThe API token doesn't hold the scope the operation requires. data.requiredScope names it. Stop and request a token with that scope; see scopes.
error.auth.unauthorized401Any operationThe API token is missing or invalid.
error.coverage.missing_patient_demographics400PUT /coverage/First name, last name, date of birth, or sex is missing from the user record.
error.internal500, SSE error eventGET /artifacts/{artifactId}/, POST /chats/{chatId}/stream/From GET /artifacts/{artifactId}/, artifact generation failed permanently: stop retrying. In a stream, the reply failed and the stream ended; see stream errors.
error.member.header_required400User-scoped operationsThe request has no X-Doctronic-User-ID header. Send the user's ID on every user-scoped request; see request headers.
error.member.not_found404User-scoped operationsThe user in X-Doctronic-User-ID doesn't exist, isn't active, or isn't one of your organization's users. All three return the same response. Check the stored patient-to-user mapping and that it comes from the same environment as the token.
error.partner.chat_not_found404GET /chats/{chatId}/, GET /chats/{chatId}/messages/, POST /chats/{chatId}/stream/The chat doesn't exist in this organization and user scope. Chats outside the scope are masked with this code.
error.partner.chat_stream_in_progress409POST /chats/{chatId}/stream/Another stream is already running for this chat.
error.partner.chat_stream_unavailable503POST /chats/{chatId}/stream/Streaming is briefly unavailable and the turn didn't start.
error.partner.validation400POST /chats/, POST /chats/{chatId}/stream/The request or its context content failed input validation.
error.pharmacy.missing_patient_demographics400PUT /pharmacy/First name, last name, date of birth, or sex is missing from the user record.
error.pharmacy.not_found404PUT /pharmacy/No pharmacy matches the NCPDP ID.
error.rate_limitedSSE error eventPOST /chats/{chatId}/stream/Model capacity is constrained. Wait at least retryAfterSeconds.
error.unknownAnyAny operationThe default code in the error schema. Handle it by HTTP status.
error.user.conflict400POST /users/, PATCH /users/A user with this email or phone number already exists. Conflicts never merge or transfer users.
error.validation.missing400, inside dataAny operation that validates inputA required field is missing. Appears as data[].code in an error.validation_error response.
error.validation.phone_number.unavailable503POST /users/, PATCH /users/Phone validation is briefly unavailable. Retry the same request after Retry-After.
error.validation_error400Any operation that validates inputThe request failed field validation. data lists each failure.

The stream can also emit error.consultation_not_ready, described under stream errors.

Idempotent requests

Send an Idempotency-Key header on a mutation so a retry can't apply it twice. These operations accept one:

OperationReference
POST /users/Create a user
PATCH /users/Update user details
POST /chats/Create a chat
POST /appointments/scheduled/Book a scheduled appointment
POST /appointments/asap/Book an ASAP appointment
POST /appointments/{appointment_id}/cancel/Cancel appointment
POST /appointments/{appointment_id}/reschedule/Reschedule appointment
PUT /address/Set a user's address
POST /pharmacy/search/Search pharmacies
PUT /pharmacy/Set a user's pharmacy
PUT /coverage/Set user insurance coverage

Generate one unique, opaque key (up to 255 characters) for each intended change. If the response is lost, retry the same method, path, and body with the same key:

IDEMPOTENCY_KEY="$(uuidgen)"

curl --include -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"}'
const idempotencyKey = crypto.randomUUID();
const body = JSON.stringify({ timezone: 'America/New_York' });

async function createChat(): Promise<Response> {
  for (let attempt = 1; ; attempt += 1) {
    try {
      const response = await fetch(`${process.env.DOCTRONIC_API_URL}/chats/`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.DOCTRONIC_API_TOKEN}`,
          'X-Doctronic-User-ID': process.env.DOCTRONIC_USER_ID!,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json',
        },
        body,
        signal: AbortSignal.timeout(10_000),
      });
      if (response.status !== 503 || attempt === 3) return response;
      const retryAfter = Number(response.headers.get('retry-after') ?? '1');
      await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
    } catch (error) {
      // Network failure or timeout: the outcome is unknown, so retry with the same key.
      if (attempt === 3) throw error;
      const backoff = 2 ** attempt * 500 + Math.random() * 250;
      await new Promise((resolve) => setTimeout(resolve, backoff));
    }
  }
}

const response = await createChat();
console.log(response.status, response.headers.get('idempotency-replayed'));

The API stores the result for 24 hours. A response served from that store includes:

Idempotency-Replayed: true
ResponseCauseWhat to do
409The key is still being processed, or it was already used with a different payload.Wait and retry the original request, or generate a new key for a different change.
503 with Retry-AfterThe API couldn't coordinate idempotency safely, so the mutation didn't start.Wait at least Retry-After seconds, then retry with the same key and body.

Use a key for exactly one request body. POST /chats/{chatId}/stream/ doesn't accept a key; see stream errors for how to recover a turn.

Stream errors

POST /chats/{chatId}/stream/ fails in two ways. Before the event stream starts, it returns an ordinary JSON error. After it starts, a failure arrives as an error event with errorCode, message, and an optional retryAfterSeconds, and the stream ends after that event:

event: error
data: {"type":"error","errorCode":"error.rate_limited","message":"The service is busy right now. Please retry in a few moments.","retryAfterSeconds":7}

Handle an error event separately from assistant content; your stream parser should route it like any other control event in the event reference.

ErrorWhere it appearsRecovery
409 error.partner.chat_stream_in_progressJSON response, before the stream startsAnother stream is running for this chat. Wait for it to finish, check GET /chats/{chatId}/messages/, then send the next turn.
503 error.partner.chat_stream_unavailableJSON response, before the stream startsThe turn didn't start. Retry shortly with bounded backoff.
error.rate_limitederror eventWait at least retryAfterSeconds when present, then check the transcript before offering a retry.
error.internalerror eventThe reply failed partway through the turn. Check the transcript, then let the patient resend the message.
error.consultation_not_readyerror event, at the end of a consultationThe consultation couldn't be finalized, so booking is unavailable for this turn and no ai_consultation.complete follows. Check the user's profile and address, then let the patient continue the chat.

Do not automatically resend a turn after an error event or a dropped connection. The first attempt may already have produced work. Check GET /chats/{chatId}/messages/ and let the patient choose to retry. The end of the HTTP connection marks transport completion only; see end states.

Timeouts and uncertain outcomes

Set finite connection and total timeouts on ordinary JSON requests. A stream stays open for the whole assistant turn, so give it a longer read timeout and an explicit cancel action. The contract doesn't define one timeout value per workflow; choose limits that fit your patient experience.

A client timeout leaves the outcome unknown:

  • For an idempotent mutation, retry with the same key and body.
  • For any other mutation, read the resource to see whether the change happened before you replay it.
  • For a stream turn, read the transcript before offering to resend the patient's message.

Request IDs

Responses carry an X-Request-ID header. Store it with your local request record, including for failures, so Doctronic can trace the request without you sending credentials or patient content. See report an API problem for what to include in a report. The SDKs expose it as requestId (TypeScript) and request_id (Python) on errors, streams, and raw responses.

Retry checklist

  • Retry only failures the operation documents as temporary: network errors, 503, other 5xx on reads, and stream errors with retryAfterSeconds.
  • Keep the same idempotency key and body when retrying a supported mutation.
  • Read the resource before replaying a mutation without an idempotency key.
  • Cap attempts and total elapsed time.
  • Use exponential backoff with jitter, and wait at least Retry-After or retryAfterSeconds when present.
  • Treat the end of a stream connection as transport completion, not consultation completion.
  • Keep X-Request-ID with every failed request.

The SDKs apply this checklist for you. They retry reads and idempotent mutations after network errors, timeouts, 429, and 503, and idempotent mutations after an idempotency conflict, always with the same key and body. They back off exponentially with jitter, wait for Retry-After up to 60 seconds, stop after maxRetries / max_retries retries (default 2), and never retry a stream. Pass your own idempotencyKey / idempotency_key to keep a key across process restarts.

On this page