Errors
The API answers errors with an HTTP status and a JSON body that names the error. Branch on the code, not the message.
Error shape
Every endpoint except the token endpoint returns errors in this shape:
{"error": {"code": "insufficient_credits", "message": "Not enough credits for this analysis."}}codeis stable within/v1. Use it in your code.messageis a sentence for people. It can change, and it never repeats what you sent: a validation error names the field, never its value, so error responses and your logs stay free of note text.
The token endpoint, POST /v1/oauth/token, follows OAuth 2.0 and returns {"error": "<code>"}. For its codes,
see Token endpoint errors.
Error codes
| Status | Code | Cause | What to do |
|---|---|---|---|
400 | invalid_request | The body or a parameter is invalid; the message names the fields. Also a body that isn't a JSON object or isn't valid JSON (or UTF-8), an unknown field, an unknown jurisdiction code, an invalid Idempotency-Key header, an invalid cursor, a time before 2000-01-01 or after 2100-01-01, or client_reference in a query string. | Fix the request. Don't retry it unchanged. |
401 | unauthorized | The access token is missing, malformed, or expired; its key was revoked; or its organization was deleted. The response has WWW-Authenticate: Bearer. | Get a new token and retry once. If it fails again, check the key. |
402 | insufficient_credits | The balance doesn't cover the note, the whole batch, or the notes to retry. Nothing is charged; for a batch, nothing is stored or held. | Buy credits on the Billing page, or send fewer notes. |
403 | baa_resign_required | The organization is past its due date to sign an updated BAA, so new notes and batches are paused. | Ask an owner to sign the updated BAA in the app, in Settings > BAA. Processing resumes as soon as it's signed. |
403 | org_suspended | MedChartScan support suspended the organization. New notes and batches (POST /v1/analyses, POST /v1/batches, batch retries) are refused; reads, search and deletes still work. | Write to support@medchartscan.io. Don't retry until support restores the organization. |
403 | org_locked | MedChartScan support locked the organization. Every request is refused, and the token endpoint issues no token. | Write to support@medchartscan.io. Don't retry. |
404 | not_found | The ID doesn't exist, is malformed, belongs to another organization, or was created in the web app. Also an idempotent replay of a result that was deleted, and an unknown path. | Check the ID. Don't retry. |
409 | idempotency_conflict | The Idempotency-Key was already used with a different body or on the other endpoint. | Use a new key for a new request. |
409 | idempotency_in_progress | The first request with this Idempotency-Key is still running. The response has Retry-After: 2. | Retry with the same key after Retry-After seconds. |
409 | not_retryable | A batch note you asked to retry didn't fail, or its 7-day retry window has passed. Nothing changes. | Retry only notes with "retryable": true. |
409 | analysis_running | You asked to delete a note that's still running. Nothing is deleted. | Delete it once it has finished. |
413 | note_too_long | A note is longer than 100,000 characters. | Shorten or split the note. |
413 | request_too_large | The request body is larger than 1 MB, or 12 MB for POST /v1/batches. | Send fewer or shorter notes per request. |
422 | test_data_required | The organization is in test mode and the request didn't send "test_data": true. | Send synthetic notes with "test_data": true, or accept the BAA. |
429 | rate_limited | The organization's per-minute limit on analyses is reached (Retry-After: 60), or the key sent more than 600 reads (GET requests and POST /v1/analyses/search) this minute. | Wait for Retry-After seconds, then retry. |
429 | too_many_uncharged_runs | 25 of the organization's notes ended out of scope or failed in the last 24 hours, or a batch would put more notes in progress than the uncharged limit allows. | Wait for Retry-After seconds, or send a smaller batch. Check why your notes fail or are out of scope. |
429 | too_many_batches | The organization has 5 unfinished batches. | Wait for a batch to finish, then retry. |
500 | internal_error | An unexpected error occurred on the server. Nothing is charged. | Retry with backoff. If it continues, contact support. |
502 | analysis_failed | The analysis couldn't be completed. Nothing is charged. | Send the note again, with a new Idempotency-Key if you used one. |
Other client errors, such as a method that a path doesn't support, return the status with the code http_error. A
405 has an Allow header that lists the methods the path supports.
Validation messages
A 400 invalid_request message names what's wrong without repeating your values:
| Message | Cause |
|---|---|
Invalid request: document_text, cpts.0.code | Those fields are missing or invalid. List positions count from 0. |
Invalid request: unknown field cpts.0.unit | A field that the endpoint doesn't take, here inside the first CPT. Requests with unknown fields are refused rather than ignored, so a misspelled field can't change what the request does. An unknown key that isn't a field name of this API shows as ?, because it could be patient data. |
Invalid request: body must be a JSON object. | The body is missing, or is an array, a string, a number, or form text. |
Invalid request: body is not valid JSON. | The body can't be parsed as JSON. |
Invalid request: body is not valid UTF-8 JSON. | The body has bytes that aren't UTF-8. |
Retry safely
Retry only the errors that can succeed later: 401 (after you get a new token), 409 idempotency_in_progress,
429, 500, and 502, plus network errors and timeouts. A 502 counts toward the
uncharged notes limit, so don't retry the same failing note many times. For a POST request, send an Idempotency-Key so that a
retry can't charge twice. For more information, see Idempotency.
Wait between attempts, and wait longer after each one: for example 2, 4, then 8 seconds, plus a random part of a
second. When the response has a Retry-After header, wait at least that many seconds.
import random
import time
import uuid
import requests
RETRYABLE = {409, 429, 500, 502}
def analyze(note: str, token: str) -> dict:
key = str(uuid.uuid4()) # one key for every attempt of this note
for attempt in range(5):
try:
resp = requests.post(
f"{API}/v1/analyses",
headers={"Authorization": f"Bearer {token}", "Idempotency-Key": key},
json={"document_text": note},
timeout=300,
)
except requests.RequestException:
resp = None
if resp is not None and resp.status_code not in RETRYABLE:
resp.raise_for_status()
return resp.json()
if resp is not None and resp.status_code == 409:
if resp.json()["error"]["code"] != "idempotency_in_progress":
resp.raise_for_status()
if resp is not None and resp.status_code == 502:
key = str(uuid.uuid4()) # the same key would replay the failure
wait = 2 ** (attempt + 1) + random.random()
if resp is not None and "Retry-After" in resp.headers:
wait = max(wait, int(resp.headers["Retry-After"]))
time.sleep(wait)
raise RuntimeError("The analysis didn't finish after 5 attempts.")