Errors
The Partner API returns errors from two layers, and they have different response shapes. Telling
them apart is the first step in debugging any failure.
Gateway errors
Our API gateway rejects some requests before they reach the application. These return a bare
message object:
{ "message": "Forbidden" }| Status | Body | Meaning | Fix |
|---|---|---|---|
| 403 | {"message":"Forbidden"} | X-API-Key header is missing or invalid | Send X-API-Key on every request |
| 403 | {"message":"Missing Authentication Token"} | The route does not exist | Check your base URL includes /v2, and check the method and path |
The second message is misleading — it is the gateway's generic response for an unmatched route and
has nothing to do with your credentials. If you see it, compare your URL against
Environments & Base URLs.
Application errors
Everything else returns the PartnerApiError envelope:
{
"error": {
"type": "BadRequestException",
"message": {
"message": [
"lastName should not be empty",
"dateOfBirth must be a valid ISO 8601 date string"
],
"error": "Bad Request",
"statusCode": 400
},
"statusCode": 400,
"timestamp": "2026-07-29T16:56:56.150Z",
"details": []
},
"path": "/v2/clinicians/:clinicianId/clients"
}| Field | Notes |
|---|---|
error.type | The internal exception class name. Do not match on this — it changes when we refactor. Use error.statusCode. |
error.message | Shape varies. For validation failures it is an object containing a message array of field errors. For business-rule failures it is a plain string. Handle both. |
error.statusCode | The HTTP status. This is the field to branch on. |
error.timestamp | ISO 8601. |
error.details | Reserved. Currently always []. |
path | The request path that failed, as seen by Blueprint's internal routing. It may not match the URL you called — treat it as a diagnostic to quote in a support request, not a value to parse or match on. |
Because error.message is either an object or a string, extract it defensively:
function describe(err) {
const m = err?.error?.message
if (typeof m === 'string') return m
if (Array.isArray(m?.message)) return m.message.join('; ')
return `HTTP ${err?.error?.statusCode ?? 'unknown'}`
}Status codes
| Status | When it happens |
|---|---|
| 400 | Request validation failed. error.message.message lists the offending fields. Also returned for a small number of business rules — for example phoneNumber should include calling code. |
| 401 | Your Access-Token has expired. Re-authenticate with POST /partners/authenticate. |
| 403 | Authentication failed. Either the gateway rejected your X-API-Key, or your Access-Token is missing, malformed, or not recognized. One application-level case remains: the progress note is locked. See below. |
| 404 | The resource in the path does not exist, or does not belong to your partner organization. The two are deliberately indistinguishable. |
| 409 | Conflict with current state — for example transferring a client to the clinician they already have, administering assessments that are already outstanding, creating a clinician whose email already exists, or unarchiving a client who is not archived. |
404 is the one to plan for
Every path parameter is checked for ownership: the organization, clinic, clinician, client, session,
progress note, summary, or assessment score you name must belong to your partner organization. If it
does not, you get 404.
A resource that does not exist and a resource that belongs to another partner return the identical
404. That is deliberate — the API never confirms whether an ID exists in someone else's data — so you
cannot tell the two apart, and should not try.
In practice a 404 on a well-formed request almost always means one of:
- You are using a sandbox ID against production, or vice versa
- You stored an ID from a different partner environment
- You constructed a UUID rather than using one Blueprint returned
Progress notes are the one application-level 403 left. Once a note is finalized it is locked, and
these return 403 with the message Progress note is locked:
PATCH /progress-notes/{progressNoteId}POST /progress-notes/{progressNoteId}/magic-editPOST /progress-notes/{progressNoteId}/regenerate
Listen for the progress_note_finalized webhook to know when a note has crossed that line. See
Webhook Events Reference.
Clinical artifacts that are not ready yet
Transcripts, summaries, progress notes, and MDM elements are generated asynchronously. Requesting
one before it exists is a normal condition, not an error, and there are two distinct signals:
Still generating. GET /sessions/{sessionId}/progress-note returns 200 with
isLoading: true and an empty or partial note array. Poll, or better, wait for the
progress_note_generated webhook.
Never generated. For GET /sessions/{sessionId}/transcript and
GET /sessions/{sessionId}/mdm, a missing artifact currently returns HTTP 200 with a body
describing the error rather than a 404. Do not rely on the status code for these two endpoints —
check whether the expected fields (transcriptItems, problemsAddressed) are present:
const res = await fetch(transcriptUrl, { headers })
const body = await res.json()
if (!body.transcriptItems) {
// not available yet — retry later or wait for `transcript_ready`
}This is a known defect and will change to a 404. Write your check against the response body, not
the status code, so a future fix does not break you.
The reliable pattern for all four artifacts is to wait for the corresponding webhook and fetch the
URL it gives you. See Listening to Webhooks.
Retries
The API does not support idempotency keys, so retries are not automatically safe. See
Pagination & Limits for what is and is not safe to retry.
Recommended handling:
| Status | Retry? |
|---|---|
| 5xx | Yes, with exponential backoff |
| 429 | Not currently returned by the Partner API, but handle it defensively |
| 401 | Once, after re-authenticating |
| 400, 403, 404, 409 | No — these are deterministic. Fix the request. |
Updated 28 days ago
