API reference · version 2026-08-22
Ambition Health API reference
Find out what you cannot sell before you sign anything: the formulary and rulebook are public. Developer operations use scoped keys, first-party operations use a console session, and pharmacy callbacks require a versioned partner signature. The specification (/v1/openapi.json on each API host, /api/v1/openapi on any deployment) is generated from the same schemas the handlers validate with, so it cannot describe a shape the API does not enforce.
New here? The quickstart goes from a sandbox key to a test patient and a signed webhook; the hand-off guide covers sending visitors from your site to your care site.
Quickstart
Production deployment These calls go to https://app.ambitionhealth.co, the deployment serving this page, on real data. Use a key issued for this deployment.
# The formulary needs no credential at all.
curl "https://app.ambitionhealth.co/api/v1/formulary?status=prohibited"
# Ask whether one substance is sellable, and why not.
curl "https://app.ambitionhealth.co/api/v1/formulary/bpc-157"
# Scan copy against the same rulebook that gates publication. The key needs
# the claims.scan scope and resolves only on the deployment that issued it.
AMBITION_API_BASE_URL="https://app.ambitionhealth.co/api/v1"
curl -X POST "$AMBITION_API_BASE_URL/claims/scan" \
-H "authorization: Bearer $AMBITION_API_KEY" \
-H "content-type: application/json" \
-d '{"text":"Same active ingredient as Ozempic."}'Sandbox keys are created in a workspace's Developer Center. The scan endpoint runs the same engine, against the same rulebook version, that gates publication inside the platform. Wire it into your CI and you find out at pull-request time instead of at launch.
API hosts and sandbox
Two planes, two hosts. The developer sandbox is a separate deployment with its own database, on synthetic data only; live is the production deployment, on real data. A key works only on the plane its prefix names. Each plane's API host at /v1 is its canonical base URL once it serves. The deployment's own origin at /api/v1 stays served as an alias, with the same routes.
| Plane | Base URL (canonical once serving) | Alias | Key prefix | Data |
|---|---|---|---|---|
| Sandbox | https://sandbox.api.ambitionhealth.co/v1 (Not serving yet) | https://aws-nonprod.ambitionhealth.co/api/v1 | ak_sandbox_ | Synthetic only |
| Live | https://api.ambitionhealth.co/v1 (Not serving yet) | https://ambitionhealth.co/api/v1 (Not serving yet) | ak_live_ | Real |
A base URL marked "Not serving yet" answers nothing. Until the API host serves, call its alias, or the origin of the deployment that issued your key, followed by /api/v1 (this deployment: https://app.ambitionhealth.co/api/v1). The paths are the same: /v1/<path> on an API host is /api/v1/<path> on the alias.
Every answer says which plane served it. Ambition-Environment is sandbox or live, and Ambition-Livemode is false or true. The deployment sets both; nothing in your request changes them. Check them in your client before you treat an answer as real data.
The OpenAPI document is at /v1/openapi.json on each API host and at /api/v1/openapi on any deployment. Its servers list both planes, sandbox first, each canonical host followed by its alias and marked with its plane and whether it serves yet; while the API hosts do not serve, the deployment that served the document comes first. An API host serves the API and nothing else: no pages, no sign-in, and no cookies.
Hosted mode guides
Hosted mode: your site sells; your Ambition-hosted care site (care.<brand>) does care. These guides cover its server-to-server path: the public catalog, enrollments and launch links, signed webhooks, and what your site may and may not hold.
On this deployment: Headless Connect API: Not available in this environment · Self-enrollment on the care host: Not open yet · Webhooks to your receivers: Sandbox only · Live keys: Not available
- Headless Connect API: Not available in this environment. What runs now: Sandbox-key calls to /api/v1 on the nonproduction deployment with synthetic data: claims scan, formulary, rulebook, the sandbox order preview, delivery reads and replay, and the MCP endpoint, on a deployment whose database has the 20261101090000-range migrations applied (a hard deploy-order dependency the merge gate enforces; ahead of them the rulebook, claims scan, MCP and replay answer 503).
- Self-enrollment on the care host: Not open yet. What runs now: Waitlist capture with consent and privacy-notice digest on /storefront/start; prescriber-issued referral invitations. Built but not yet verified end to end and open for no brand: self-enrollment on the care host (/storefront/start?p=<handle>: email code, account, consent recorded by notice digest, admission under brand + program + state enrollment authority, intake for that program) and launch-link redemption (/storefront/launch), behind each brand's hosted.self_enrollment capability row on the deployment's own plane. A brand's production row is set live only by ah_set_brand_capability_state (a platform administrator at AAL2 with a reason, recorded in ah_capability_state_events).
- Webhooks to your receivers: Sandbox only. What runs now: Signing, retries (19 attempts over at least about 74 hours; every 4xx but 410 retried, 410 disables the receiver), dead letters, replay and delivery receipts against the in-process synthetic receiver (.demo.invalid) on synthetic deployments; a self-serve developer connection for brands with no partner integration; and, built but not yet exercised on a deployed sandbox, sandbox delivery of synthetic events to a developer's own https receiver while the developer_webhooks sandbox rail is open; and, written and unit-tested but with its migration not yet applied and its pgTAP suite not yet run, lifecycle event producers with refs-only payloads carrying the enrollment id and externalReference (enrollment.created, subscription.created, subscription.canceled, payment.failed, and enrollment.status_changed produced only under the brand-to-entity BAA and downstream-systems attestation and re-checked at every send).
- Live keys: Not available. What runs now: Nothing on any deployment: the governed request, approval and issuance path (20261101090100) is written but not applied to a hosted database or run, and no production database has recorded its plane.
- Quickstart (hosted mode): a test patient in the sandbox From a sandbox key to a test patient a simulated clinician approved, with the enrollment.status_changed webhook received.
- Sandbox and live: planes and keys Two deployments, two key prefixes, and what a key can carry.
- Idempotency Retry a write safely, and what a replayed enrollment returns.
- Rate limits Per-key and per-address budgets, the headers that report them, and how to back off.
- Hosted mode: the hand-off contract Hosted mode. Your site sells, your care site does care: links, launch links, return URLs and logins.
- Compliance Kit Claim scans, required disclosures, external sites, status data under a BAA, and no ad-platform forwarding.
- Webhooks The signed string, verifiers, the retry schedule and every event’s payload.
Headless mode guides
Headless mode, the Hims/Ro model: your own site and app run the whole patient experience (account, intake, consent, checkout, portal, messaging) and your Node server calls the patient endpoints (tagged Patient (PHI) in the reference) with a phi_server key, once your patient data access is approved. Ambition is the clinical engine behind it.
On this deployment: Headless clinical API (patient data): Not available · Webhooks to your receivers: Sandbox only · Live keys: Not available
- Headless clinical API (patient data): Not available. What runs now: Nothing for a real patient: the headless patient API is written and has run only against a local synthetic stack; no brand's patient data access is approved in any environment, and the Developer Center's clinical-loop simulator is a script with synthetic ids.
- Webhooks to your receivers: Sandbox only. What runs now: Signing, retries (19 attempts over at least about 74 hours; every 4xx but 410 retried, 410 disables the receiver), dead letters, replay and delivery receipts against the in-process synthetic receiver (.demo.invalid) on synthetic deployments; a self-serve developer connection for brands with no partner integration; and, built but not yet exercised on a deployed sandbox, sandbox delivery of synthetic events to a developer's own https receiver while the developer_webhooks sandbox rail is open; and, written and unit-tested but with its migration not yet applied and its pgTAP suite not yet run, lifecycle event producers with refs-only payloads carrying the enrollment id and externalReference (enrollment.created, subscription.created, subscription.canceled, payment.failed, and enrollment.status_changed produced only under the brand-to-entity BAA and downstream-systems attestation and re-checked at every send).
- Live keys: Not available. What runs now: Nothing on any deployment: the governed request, approval and issuance path (20261101090100) is written but not applied to a hosted database or run, and no production database has recorded its plane.
- Headless, hosted or hybrid: choosing a mode Run the whole patient experience yourself (headless, the Hims/Ro model), let Ambition host it, or mix them per program.
- Quickstart (headless mode): one synthetic patient through the clinical loop From a sandbox phi_server key to a shipped synthetic order: identity, consent, intake, labs, decision, release, fills and webhooks.
- Your responsibilities when you hold patient data What a headless brand must do as a business associate: keys, pages, vendors, logs, notices and records requests.
- The clinical-loop simulator and certification (sandbox) Run your sandbox patients through the real loop: the sandbox's synthetic clinician, pharmacy and clock act through the real commands and your receiver gets the real events.
- Referrals: enrolling a patient you created Refer your patient to the medical group that covers your brand; the group accepts, the patient accepts with consent and the program terms, and intake opens.
- Sign-in links: a one-time link to your own site For a brand without its own sign-in: Ambition emails your patient's verified address a one-time link to a hand-off page of your site, and your server confirms it.
Public — no credential required
/api/v1/formularyno authList every substance the platform has a position on
The authoritative record of what this platform permits, refuses, and permits with conditions. Every row carries the provision that governs it, and every refusal carries a reason in plain language. This endpoint is public and unauthenticated on purpose: an operator should be able to learn what they cannot sell before they sign anything.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
status | "approved" | "approved_off_label" | "compoundable" | "conditional" | "prohibited" | no | Only rows with this status. |
category | string | no | Only rows in this category. |
search | string | no | Matches the name and aliases. |
limit | integer | no | Rows per page, 1 to 200. |
cursor | string | no | The `nextCursor` of the previous page. |
Example response · 200
{
"data": [
{
"slug": "example-slug",
"displayName": "example-display-name",
"aliases": [
"example-aliases"
],
"category": "example-category",
"status": "approved",
"controlledSchedule": "II",
"approvedProducts": [
{
"brand": "example-brand",
"application": "example-application",
"approved": "example-approved"
}
],
"compounding": {
"status503a": "example-status503a",
"status503b": "example-status503b",
"bulkBasis": "example-bulk-basis",
"note": "example-note"
},
"citation": "example-citation",
"conditions": [
"example-conditions"
],
"refusalReason": "example-refusal-reason",
"reviewBy": "example-review-by"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"formularyVersion": "example-formulary-version",
"total": 1,
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Query parameters were invalid. |
/api/v1/formulary/{slug}no authRead one substance, with its citation and refusal reason
Returns the platform's position on a single substance. Also resolves aliases, so `Viagra` and `sildenafil citrate` both reach the `sildenafil` row.
Example response · 200
{
"data": {
"slug": "example-slug",
"displayName": "example-display-name",
"aliases": [
"example-aliases"
],
"category": "example-category",
"status": "approved",
"controlledSchedule": "II",
"approvedProducts": [
{
"brand": "example-brand",
"application": "example-application",
"approved": "example-approved"
}
],
"compounding": {
"status503a": "example-status503a",
"status503b": "example-status503b",
"bulkBasis": "example-bulk-basis",
"note": "example-note"
},
"citation": "example-citation",
"conditions": [
"example-conditions"
],
"refusalReason": "example-refusal-reason",
"reviewBy": "example-review-by"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | not_found | No substance matches that slug or alias. |
/api/v1/rulebookno authRead the active marketing-claim rulebook
Every rule the claim scanner enforces, with the statute or enforcement instance behind it. Published so that a tenant can audit the gate rather than discover it at publish time. Patterns are not published. A rule with `detection: alias_pair` reads two different substances of an alias group joined together (`aliasGroups` lists which substances each group joins).
Example response · 200
{
"data": {
"version": "example-version",
"description": "example-description",
"sourceCitation": "example-source-citation",
"activatedAt": "2026-01-01T00:00:00.000Z",
"rules": [
{
"code": "example-code",
"severity": "block",
"category": "example-category",
"message": "example-message",
"rewrite": "example-rewrite",
"citation": "example-citation",
"rationale": "example-rationale",
"channels": [
"example-channels"
],
"detection": "pattern",
"pairGroup": "example-pair-group"
}
],
"aliasGroups": [
{
"name": "example-name",
"substances": [
"example-substances"
]
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 503 | rulebook_unavailable | No active rulebook is loaded. |
/api/v1/lifecyclesno authList every lifecycle in the product
Every record that moves between named states (a case, an order, a subscription, a site version, a webhook delivery), grouped by product area, with the status values each can hold. Read one in full at `/lifecycles/{id}` to learn what each status means and what can happen next, so a client never has to hard-code a lifecycle from prose.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
area | "care" | "orders" | "money" | "launch" | "builder" | "platform" | no | Only this product area. |
Example response · 200
{
"data": {
"total": 1,
"areas": [
{
"area": "care",
"title": "example-title",
"lifecycles": [
{
"id": "example_id",
"title": "example-title",
"subject": "example-subject",
"area": "care",
"states": [
null
]
}
]
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Query parameters were invalid. |
/api/v1/lifecycles/{id}no authRead one lifecycle: its states in words and every move
Each state with the words a person is shown, one sentence on what it means, who is being waited on, whether it is final, and every move out of it with who can make it. The database decides whether a move happens; this describes what it will accept.
Example response · 200
{
"data": {
"id": "example_id",
"title": "example-title",
"subject": "example-subject",
"area": "care",
"stored_in": "example-stored_in",
"event_log": "example-event_log",
"initial": [
"example-initial"
],
"terminal": [
"example-terminal"
],
"states": [
{
"state": "example-state",
"label": "example-label",
"meaning": "example-meaning",
"terminal": false,
"waiting_on": "patient",
"next": [
{
"to": "example-to",
"label": "example-label",
"by": [
null
],
"via": "example-via"
}
]
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | not_found | No lifecycle has that id. |
/api/v1/oauth/registerno authRegister an agent platform as an OAuth client
OAuth 2.0 Dynamic Client Registration (RFC 7591) for the MCP endpoint's authorization server. Registers a public client (no secret; PKCE S256 is required at authorization) with one to five exact redirect URIs: HTTPS, or a loopback address for a native client. Registration grants nothing: a client acts for nobody until a signed-in person consents with a fresh second factor. Answers in the RFC 7591 shapes, not the v1 envelope. Discovery: `/.well-known/oauth-authorization-server`.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
client_name | string | no | |
redirect_uris | array | yes | |
grant_types | array | no | |
response_types | array | no | |
token_endpoint_auth_method | string | no | |
scope | string | no |
Example response · 201
{
"client_id": "example_client_id",
"client_id_issued_at": 1,
"client_name": "example-client_name",
"redirect_uris": [
"example-redirect_uris"
],
"grant_types": [
"example-grant_types"
],
"response_types": [
"example-response_types"
],
"token_endpoint_auth_method": "none"
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | invalid_request | An OAuth error body (`error`, `error_description`, RFC 6749 5.2): a parameter is missing, repeated or malformed, or the grant is not valid (`invalid_grant`). |
| 401 | invalid_client | No registered client_id was sent. |
| 503 | temporarily_unavailable | The authorization server could not answer. Nothing was granted. |
| 400 | invalid_redirect_uri | A redirect URI is not an exact HTTPS URL or a loopback address. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/oauth/tokenno authExchange an authorization code or a refresh token
The OAuth 2.1 token endpoint (`application/x-www-form-urlencoded`). `authorization_code` needs the code, the exact redirect URI and the PKCE `code_verifier`; `refresh_token` rotates a single-use refresh token (one presented twice ends the connection). Tokens are bound to this server's MCP endpoint (`resource`, RFC 8707). Access tokens last 15 minutes; a connection lasts at most eight hours, like a console session. Answers in the RFC 6749 shapes, never cached.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
grant_type | "authorization_code" | "refresh_token" | yes | |
client_id | string | yes | |
code | string | no | |
code_verifier | string | no | |
redirect_uri | string | no | |
refresh_token | string | no | |
resource | string | no | RFC 8707: this server's MCP endpoint, the token's only audience. |
Example response · 200
{
"access_token": "example-access_token",
"token_type": "Bearer",
"expires_in": 1,
"refresh_token": "example-refresh_token",
"scope": "example-scope"
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | invalid_request | An OAuth error body (`error`, `error_description`, RFC 6749 5.2): a parameter is missing, repeated or malformed, or the grant is not valid (`invalid_grant`). |
| 401 | invalid_client | No registered client_id was sent. |
| 503 | temporarily_unavailable | The authorization server could not answer. Nothing was granted. |
| 400 | unsupported_grant_type | Only authorization_code and refresh_token are supported. |
| 400 | invalid_target | The resource is not this server's MCP endpoint. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/oauth/revokeno authRevoke an agent connection
OAuth token revocation (RFC 7009, form body): either token of a connection ends the whole connection. An unknown token is answered 200, as the RFC requires.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | yes | |
client_id | string | yes | |
token_type_hint | string | no |
Example response · 200
{}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | invalid_request | An OAuth error body (`error`, `error_description`, RFC 6749 5.2): a parameter is missing, repeated or malformed, or the grant is not valid (`invalid_grant`). |
| 401 | invalid_client | No registered client_id was sent. |
| 503 | temporarily_unavailable | The authorization server could not answer. Nothing was granted. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/statusno authService objectives, measured, and every published incident
Public and unauthenticated. Each objective reports the ratio actually measured over its rolling window rather than the one we would like to claim, and an objective with no observations behind it reports `unmeasured` rather than met. Resolved incidents carry their customer impact and root cause, and the update timeline is append-only.
Example response · 200
{
"data": {
"objectives": [
{
"component": "example-component",
"metric": "example-metric",
"target": 1,
"windowDays": 1,
"measured": 1,
"errorBudgetRemaining": 1,
"state": "met"
}
],
"incidents": [
{
"reference": "example-reference",
"title": "example-title",
"severity": "sev1",
"component": "example-component",
"status": "investigating",
"startedAt": "2026-01-01T00:00:00.000Z",
"resolvedAt": "2026-01-01T00:00:00.000Z",
"customerImpact": "example-customer-impact",
"updates": [
{
"status": "example-status",
"body": "example-body",
"postedAt": "2026-01-01T00:00:00.000Z"
}
]
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 503 | status_unavailable | Status could not be read. |
/api/v1/catalog/{brand}/programsno authList a brand's programs with their approved offers
Every program the brand offers on this deployment whose approved offer and approved disclosure both resolve, with the opaque program handle to link the care host's Start page with, the offer exactly as the hosted payment flow bills it, and the digest of the disclosure that must accompany it. A program with no approved offer is omitted rather than shown with a default price. No clinician, program key or clinical content is returned. Public and unauthenticated: no credential is read, the answer is the same for every caller, and it is cacheable (`Cache-Control: public, max-age=60`) with `Access-Control-Allow-Origin: *` and no credentials, so a brand's own site may fetch it from the browser. The brand handle is the brand's stable slug. An unknown brand answers 404 after the same response floor as a known one.
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"programs": [
{
"handle": "example-handle",
"name": "example-name",
"offer": {
"amountCents": 1,
"currency": "usd",
"interval": "week",
"trialDays": 1,
"display": "example-display",
"offerVersion": 1,
"termsHash": "example-terms-hash",
"effectiveTo": "example-effective-to"
},
"disclosure": {
"version": "example-version",
"digest": "example-digest"
}
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 503 | catalog_unavailable | The catalog could not be read. Nothing was changed; retry. |
/api/v1/catalog/{brand}/programs/{handle}no authRead one program's approved offer by its public handle
The same entry the list returns, for one program handle. A handle that names no program of the brand, or a program with no approved offer, answers 404. Public and unauthenticated: no credential is read, the answer is the same for every caller, and it is cacheable (`Cache-Control: public, max-age=60`) with `Access-Control-Allow-Origin: *` and no credentials, so a brand's own site may fetch it from the browser. The brand handle is the brand's stable slug. An unknown brand answers 404 after the same response floor as a known one.
Example response · 200
{
"data": {
"handle": "example-handle",
"name": "example-name",
"offer": {
"amountCents": 1,
"currency": "usd",
"interval": "week",
"trialDays": 1,
"display": "example-display",
"offerVersion": 1,
"termsHash": "example-terms-hash",
"effectiveTo": "example-effective-to"
},
"disclosure": {
"version": "example-version",
"digest": "example-digest"
}
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | program_not_found | No program with an approved offer has that handle in this brand's catalog. |
| 503 | catalog_unavailable | The catalog could not be read. Nothing was changed; retry. |
/api/v1/catalog/{brand}/programs/{handle}/availabilityno authWhether a program is offered in a state
`available`, `video_required` or `unavailable`, with a reason code. It reads the program's current state policy, the California exclusion (always `unavailable`, `state_excluded`), and whether any licensed clinician covers the program there. Coverage is reported as presence only: the answer never names, identifies or counts a clinician. An availability answer is not a medical diagnosis, autonomous eligibility, or production clinical approval: it says only whether the program is offered in that state. Whether a patient can be treated is decided by a licensed clinician on the care host, after intake. Public and unauthenticated: no credential is read, the answer is the same for every caller, and it is cacheable (`Cache-Control: public, max-age=60`) with `Access-Control-Allow-Origin: *` and no credentials, so a brand's own site may fetch it from the browser. The brand handle is the brand's stable slug. An unknown brand answers 404 after the same response floor as a known one.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | A two-letter USPS state code, or DC. |
Example response · 200
{
"data": {
"program": "example-program",
"state": "AL",
"status": "available",
"reason": "state_excluded"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | `state` must be a two-letter USPS state code or DC. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | program_not_found | No program with an approved offer has that handle in this brand's catalog. |
| 503 | catalog_unavailable | The catalog could not be read. Nothing was changed; retry. |
/api/v1/catalog/{brand}/disclosuresno authThe disclosures a brand's pages must show, verbatim
Every disclosure the builder would require on a hosted page, for the brand's own site: each program's approved disclosure and cancellation terms, its regulatory standing when its substance stands on a regulatory policy, the emergency notice, and the legal documents to link. Each block has a stable id, a digest, the verbatim text and a placement hint, and the response carries the same blocks as a sanitized HTML fragment. The programs are exactly the catalog's; an `approved_disclosure` digest equals the catalog's `disclosure.digest`. A regulatory block is `not_available` when the policy is withdrawn or the substance is held, and the program's availability is then `regulatory_unavailable`. No program key, clinician or clinical content is returned. Public and unauthenticated: no credential is read, the answer is the same for every caller, and it is cacheable (`Cache-Control: public, max-age=60`) with `Access-Control-Allow-Origin: *` and no credentials. An unknown brand answers 404 after the same response floor as a known one.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
program | string | no | Only this program's blocks, and the brand-wide ones. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | no | A two-letter USPS state code or DC. A regulatory block then also says whether the program is offered there. |
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"program": "example-program",
"state": "AL",
"digest": "example-digest",
"blocks": [
{
"id": "example_id",
"kind": "approved_disclosure",
"program": "example-program",
"status": "required",
"reason": "announced",
"version": "example-version",
"digest": "example-digest",
"text": "example-text",
"links": [
{
"kind": "privacy",
"label": "example-label",
"url": "example-url",
"version": "example-version",
"digest": "example-digest",
"legalEntityName": "example-legal-entity-name"
}
],
"placement": "with_price",
"draft": false
}
],
"html": "example-html"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | `program` must be a program handle and `state` a two-letter USPS state code or DC. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | program_not_found | No program with an approved offer has that handle in this brand's catalog. |
| 503 | disclosures_unavailable | The disclosures could not be read and verified. Nothing was changed; retry. |
/api/v1/catalog/{brand}/care-networkno authThe brand's care network, for its consumer terms
The legal names of the physician groups that cover the brand (with their clinicians' licensed states and their messaging SLA), the network pharmacies, and the labs. Name them in your consumer terms. Public, PHI-free and cacheable; no clinician, count, address or identifier. California is excluded.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | no |
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"state": "AL",
"physicianGroups": [
{
"legalName": "example-legal-name",
"doingBusinessAs": "example-doing-business-as",
"formationState": "example-formation-state",
"states": [
"example-states"
],
"messagingSla": {
"routineResponseHours": 1,
"urgentResponseHours": 1
}
}
],
"pharmacies": [
{
"name": "example-name",
"states": [
"example-states"
],
"coldChain": false
}
],
"labs": [
{
"legalName": "example-legal-name",
"cliaNumber": "example-clia-number"
}
],
"labsNote": "example-labs-note",
"californiaExcluded": true
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | `state` must be a two-letter USPS state code or DC. |
| 404 | brand_not_found | No brand is served under that handle on this deployment. |
| 503 | care_network_unavailable | The catalog could not be read. Retry. |
/api/v1/records-requestsno authAsk for a copy of your records (patients)
The Ambition-run records request, for a patient whose brand cannot serve them. If the email is the one Ambition verified for a patient of the brand, a code is sent to it from the physician group. The answer is the same either way.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
brand | string | yes | The brand the patient used. |
email | string | yes | The email Ambition verified for the patient. |
Example response · 202
{
"data": {
"requestToken": "example-request-token",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | The body was invalid. |
| 503 | records_request_unavailable | The request could not be taken. Retry. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/records-requests/confirmno authConfirm a records request with the emailed code
Opens a right-of-access request with the physician group, answered within 30 days. Five attempts per code.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
requestToken | string | yes | |
code | string | yes | |
details | string | no | What records the patient wants, in their words. |
Example response · 200
{
"data": {
"status": "opened",
"reference": "example-reference",
"dueAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | The body was invalid. |
| 503 | records_request_unavailable | The request could not be confirmed. Retry. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/catalog/{brand}/programs/{handle}/questionnaireno authThe program's current questionnaire, as FHIR R4
The published intake (or check-in) questionnaire of a program, as a FHIR R4 Questionnaire for your own intake screens: items with linkId, type, text, required, answerOption and enableWhen, plus Ambition extensions for the stop rules (show their patient message when the answers match), the consent scopes the program requires, the identity gate and, with `state`, the modality there. `id` is the version id and `version` its content hash; submit against both. Public and non-PHI: cacheable for 60 seconds with an ETag of the content hash.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
kind | "intake" | "check_in" | no | `intake` (default) or `check_in`. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | no | With a state, the `required-modality` extension says async or sync_video there. |
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"programHandle": "example-program-handle",
"questionnaire": {
"resourceType": "Questionnaire",
"id": "example_id",
"version": "example-version",
"status": "active",
"item": [
{}
]
}
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | program_not_found | No program has that handle in this brand's catalog. |
| 404 | questionnaire_not_found | The program has no published questionnaire of that kind. |
| 503 | intake_catalog_unavailable | The questionnaire could not be read. Retry. |
/api/v1/catalog/{brand}/questionnaires/{versionId}no authOne questionnaire version, as FHIR R4 (immutable)
A published or superseded questionnaire version by id, as a FHIR R4 Questionnaire. A version never changes, so this is the reference for answers you stored. Public and non-PHI.
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"programHandle": "example-program-handle",
"questionnaire": {
"resourceType": "Questionnaire",
"id": "example_id",
"version": "example-version",
"status": "active",
"item": [
{}
]
}
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | questionnaire_not_found | No questionnaire version with that id in this brand. |
| 503 | intake_catalog_unavailable | The questionnaire could not be read. Retry. |
/api/v1/catalog/{brand}/consent-documentsno authThe physician group's consent documents for a program and state
The legal kit a patient accepts before intake, issued by the physician group covering the brand: telehealth consent (your brand named as third-party beneficiary), the Notice of Privacy Practices acknowledgment, the compounded medication and interim peptide policy acknowledgment (programs that sell a formulary substance), the cash-pay and Medicare TrOOP certification, clinician-messaging consent and the AI-use disclosure. Each with its verbatim text and label, their SHA-256 digests, presentation rules and the capture mode in force. Public and non-PHI.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
program | string | yes | The program's opaque public handle (`prg_` and 24 hex characters). Link the care host's Start page with it: `/start?p=<handle>`. It never changes and carries no program or condition word. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | A two-letter USPS state code or DC. |
locale | string | no | A BCP 47 language-region tag, e.g. `en-US`. |
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"program": "example-program",
"state": "example-state",
"locale": "example-locale",
"classification": "synthetic",
"documents": [
{
"scope": "telehealth_consent",
"title": "example-title",
"available": false,
"documentId": "00000000-0000-4000-8000-00000000e001",
"version": 1,
"jurisdiction": "example-jurisdiction",
"locale": "example-locale",
"documentTitle": "example-document-title",
"sha256": "example-sha256",
"body": "example-body",
"labelText": "example-label-text",
"labelSha256": "example-label-sha256",
"presentation": {
"separateCheckbox": false,
"noPrefill": false,
"mustDisplayFullText": false
},
"brandBeneficiary": false,
"sandboxTemplate": false,
"legalEntityName": "example-legal-entity-name",
"effectiveAt": "2026-01-01T00:00:00.000Z",
"captureMode": "brand_rendered"
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | program_not_found | No program has that handle in this brand's catalog. |
| 503 | intake_catalog_unavailable | The documents could not be read. Retry. |
/api/v1/catalog/{brand}/safety-screenno authThe versioned emergency screen for safety reports
The emergency copy and the three seriousness questions to show before a patient reports a side effect or a product problem. Echo `version` as `emergencyScreenVersion`. The same for every brand; public and non-PHI.
Example response · 200
{
"data": {
"version": "example-version",
"sha256": "example-sha256",
"draft": false,
"emergencyText": "example-emergency-text",
"questions": [
{
"id": "emergency_symptoms",
"text": "example-text",
"answers": [
"example-answers"
]
}
],
"medwatchNotice": "example-medwatch-notice"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | The request body or parameters were invalid. |
/api/v1/catalog/{brand}/programs/{handle}/regulatoryno authA program's regulatory standing: substance, policy, products, state availability, labs
Everything your storefront needs to list or pull a regulated SKU: the substance and the regulatory policy it stands on (status, label to show verbatim, citation, Appendix B routes), the patient disclosure with its version and digest, the active pharmacy products with their standing, availability in every state with a reason, and the program's required labs. Subscribe to `formulary.policy_changed` and `formulary.state_availability_changed` and re-read this when one arrives. Public regulatory reference data: no patient, clinician or price. Cacheable (`Cache-Control: public, max-age=60`) with `Access-Control-Allow-Origin: *`; an unknown brand answers 404 after the same response floor as a known one.
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"program": {
"handle": "example-handle",
"name": "example-name",
"substance": {
"slug": "example-slug",
"name": "example-name",
"formularyStatus": "example-formulary-status",
"singleIngredient": true
},
"policy": {
"key": "example-key",
"status": "announced",
"regulatoryStatus": "example-regulatory-status",
"label": "example-label",
"citation": "example-citation",
"sourceUrl": "example-source-url",
"allowedRoutes": [
"example-allowed-routes"
],
"intermittentCourse": false
},
"disclosure": {
"version": "example-version",
"digest": "example-digest",
"text": "example-text",
"reportingInstruction": "example-reporting-instruction"
},
"products": [
{
"productId": "00000000-0000-4000-8000-00000000e001",
"form": "example-form",
"route": "example-route",
"strength": "example-strength",
"standing": "policy_withdrawn"
}
],
"regulatoryGated": false,
"states": [
{
"state": "AL",
"available": false,
"reason": "policy_not_in_effect"
}
],
"requiredLabs": [
{
"key": "example-key",
"label": "example-label",
"kind": "baseline",
"loincCodes": [
"example-loinc-codes"
],
"minCodesPresent": 1,
"maxAgeDays": 1,
"intervalDays": 1,
"noticeDays": 1,
"acceptedSources": [
"brand_arranged"
]
}
]
}
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | Too many requests from this network address. Retry after the interval in `Retry-After`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | brand_not_found | No brand catalog is served under that handle on this deployment. |
| 404 | program_not_found | No program of this brand has that handle on this deployment. |
| 503 | catalog_unavailable | The catalog could not be read. Nothing was changed; retry. |
Authenticated
/api/v1/claims/scanclaims.scanEvaluate marketing copy against the rulebook before you publish it
Runs the same engine, against the same active rulebook, that gates publication inside the platform — so a result here is the result you will get at publish time. Rules that apply only to other channels than `context` are not applied. Each hit carries the matched text, the provision behind the rule, and a compliant rewrite where one exists. With `programKey`, the copy is judged for that program: when it sells a substance on a regulatory policy, the rules for such a program's pages run too (`regulatedContext`); without it they run whenever your brand has such a program, as they would on its pages. Every scan stores a receipt (`receiptId`) with the rulebook version, the channel, the request id and the SHA-256 of the text; the text itself, and the matched fragments, are stored only when `retainText` is true. `requiredDisclosures` are checked by exact text against their current approved version. This checks claims only: the builder's price-book conflict check does not run here, so render prices from the catalog rather than writing them into copy. Do not include patient information in the text.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | Marketing copy to evaluate. |
context | "landing_page" | "email" | "sms" | "paid_ad" | "social" | "packaging" | no | Where the copy will appear; some rules apply only to some channels. |
programKey | string | no | The key of the program this copy sells. When that program sells a substance on a regulatory policy (such as the HHS interim-policy peptides), the rules for copy on such a program's pages run as well, so a claim like "Heal injuries faster" blocks without naming the substance. Without it they run whenever your brand has any such program. A key that is not one of your programs is refused. |
url | string | no | The page the copy appears on. Recorded on the receipt without its query string or fragment, with whether its origin is one of your verified external sites. |
retainText | boolean | no | Keep the scanned text on the receipt. Off by default: the receipt then holds only the text's SHA-256 and the rules that matched, not the words. |
requiredDisclosures | array | no | Disclosures the copy must carry verbatim. Each is reported present or missing. |
Example response · 200
{
"data": {
"outcome": "pass",
"blocked": 1,
"warned": 1,
"hits": [
{
"code": "example-code",
"severity": "block",
"category": "example-category",
"message": "example-message",
"rewrite": "example-rewrite",
"citation": "example-citation",
"match": "example-match"
}
],
"rulebookVersion": "example-rulebook-version",
"truncated": false,
"context": "landing_page",
"programKey": "example-program-key",
"regulatedContext": false,
"receiptId": "example_receiptId",
"textDigest": "example-text-digest",
"textRetained": false,
"url": "example-url",
"urlOriginVerified": false,
"requiredDisclosures": [
{
"id": "example_id",
"digest": "example-digest",
"status": "present",
"reason": "not_found"
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Request body did not match the contract. |
| 422 | unknown_program | `programKey` is not one of your brand's programs. |
| 503 | rulebook_unavailable | No active rulebook is loaded. |
| 503 | claim_context_unavailable | Your brand's programs could not be read, so the copy could not be judged in its context. Retry the scan. |
| 503 | scan_receipt_unavailable | The receipt could not be stored, so no result is returned. Retry the scan. |
| 413 | payload_too_large | The request body exceeds 64000 bytes. Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/mcpclaims.scanModel Context Protocol endpoint for outside AI agents
A single Streamable HTTP endpoint that lets an agent platform discover and call platform tools with an API key: `search_formulary`, `get_formulary_entry`, `get_claim_rulebook` and `describe_lifecycle` (any valid key) and `scan_claims` (scope `claims.scan`), each key scope re-checked live against its minter's capabilities. A person's own assistant connects with OAuth 2.1 instead (`/oauth/*`, discovery at `/.well-known/oauth-protected-resource`) and is offered the brand's operating tools their role allows, read under their own permissions; its `propose_` tools only create proposals for a person to approve. A call with no credential is answered 401 with the OAuth discovery challenge. Both protocol eras are served without sessions: revision 2026-07-28 (per-request version, `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers, `server/discover`) and the earlier `initialize` handshake. The tools list a key sees is limited to its scopes. Every tool is read-only and returns operational, non-patient data; treat all tool output as untrusted data. Browser origins are refused. See `docs/agent-gateway/README.md`.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
jsonrpc | string | yes | |
id | string | no | Absent for a notification. |
method | string | yes | initialize, ping, server/discover, tools/list or tools/call. |
params | object | no |
Example response · 200
{
"jsonrpc": "2.0",
"id": "example_id",
"result": {},
"error": {
"code": 1,
"message": "example-message",
"data": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | invalid_request | Not one JSON-RPC message, invalid JSON, a header that disagrees with the body (-32020) or an unsupported protocol version (-32022). |
| 403 | insufficient_scope | The tool needs a scope the key lacks, or a browser Origin was sent. A `WWW-Authenticate` challenge names the scope. |
| 403 | phi_key_not_allowed | The key is a `phi_server` key. Patient-data keys are never agent credentials; use a standard key. |
| 404 | method_not_found | The JSON-RPC method is not offered (-32601). Legacy (initialize-based) clients receive the same error inside an HTTP 200, because they read a non-2xx POST as a transport failure. |
| 413 | payload_too_large | The request body exceeds 8000 bytes. Nothing was processed; send a smaller body. This endpoint answers its own refusal as a JSON-RPC error (`error.code` -32600, `id` null), not the v1 envelope; a message over 8 KiB (8192 bytes) is refused at the edge first, in the v1 envelope with `request_id` `edge`. Send longer copy to `POST /claims/scan`. |
/api/v1/deliveriesdeliveries.readEvery webhook delivery attempt made on your behalf
One row per attempt, successes included — a log that records only failures cannot answer "did you send it?", which is the question an incident actually asks. Each row carries the response code, the error, whether the payload was signed, when the next attempt is due, and the delivery, event id and receiver it belongs to. Newest first; pass `meta.nextCursor` as `startingAfter` for the next page. The first page counts the attempts matching the filters in `meta.total` (up to 10,000, with `meta.totalCapped` when there are more); later pages answer null.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
status | "delivered" | "retrying" | "dead_letter" | "duplicate" | "reconciled" | "rejected" | no | Only attempts with this outcome. |
orderId | string | no | Only attempts for this order. |
eventType | string | no | Only attempts of this event type. |
endpointId | string | no | Only attempts sent to this receiver. |
createdAfter | string | no | Only attempts made at or after this time (ISO 8601). |
createdBefore | string | no | Only attempts made before this time (ISO 8601). |
startingAfter | string | no | The `nextCursor` of the previous page. Pages run newest first; an attempt recorded while you page can land before your cursor, so dedupe on `ambition-event-id` and use GET /events for completeness. |
limit | integer | no | Attempts per page, 1 to 200. |
Example response · 200
{
"data": [
{
"id": "example_id",
"requestId": "example_requestId",
"eventType": "example-event-type",
"orderId": "example_orderId",
"status": "delivered",
"attempt": 1,
"responseCode": 1,
"errorCode": "example-error-code",
"detail": "example-detail",
"signatureValid": false,
"nextAttemptAt": "2026-01-01T00:00:00.000Z",
"occurredAt": "2026-01-01T00:00:00.000Z",
"outboxId": "example_outboxId",
"eventId": "example_eventId",
"endpointId": "example_endpointId"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"total": 1,
"totalCapped": false,
"hasMore": false,
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Query parameters were invalid, or the cursor was not one this API issued. |
/api/v1/deliveries/{id}deliveries.readOne webhook delivery with its attempts
A delivery of one event to one of your receivers: its status, every attempt (number, time, response code and class, failure class, next retry), when the next attempt is due, and its redeliveries. Never a payload, a response body or a secret.
Example response · 200
{
"data": {
"id": "example_id",
"eventId": "example_eventId",
"requestId": "example_requestId",
"eventType": "example-event-type",
"orderId": "example_orderId",
"endpointId": "example_endpointId",
"status": "pending",
"attemptCount": 1,
"maxAttempts": 1,
"nextAttemptAt": "2026-01-01T00:00:00.000Z",
"createdAt": "2026-01-01T00:00:00.000Z",
"deliveredAt": "2026-01-01T00:00:00.000Z",
"replayOf": "example-replay-of",
"replayReason": "example-replay-reason",
"attempts": [
{
"attempt": 1,
"occurredAt": "2026-01-01T00:00:00.000Z",
"status": "delivered",
"responseCode": 1,
"responseClass": "1xx",
"failureClass": "example-failure-class",
"signatureValid": false,
"nextAttemptAt": "2026-01-01T00:00:00.000Z"
}
],
"redeliveries": [
{
"id": "example_id",
"status": "pending",
"createdAt": "2026-01-01T00:00:00.000Z"
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | not_found | No such delivery for this key's brand and environment. A sandbox key cannot read a production delivery. |
| 503 | delivery_log_unavailable | The delivery could not be read. Nothing was changed; retry. |
/api/v1/deliveries/replaydeliveries.replaySend a delivered or dead-lettered event again, without opening a support ticket
Queues a fresh attempt at the same event for the receiver it went to: by `outboxId`, or by the `eventId` your receiver saw (with `endpointId` when the event went to several receivers). The event id and payload are preserved, so a receiver that deduplicates on `ambition-event-id` will not act twice. Works on a delivered delivery and on a dead-lettered one; a delivery still being retried, or a receiver that is paused or disabled, is refused. Asking again while the redelivery is queued returns it and costs nothing. Naming a dead-lettered delivery's `outboxId` after it was replayed returns that replay (`created: false`), even once it was delivered: redeliver by `eventId` to send the newest delivery again. A clinical event needs a phi_server key with `clinical_events.read`, a verified clinical receiver and the patient data gate now, and each such request is recorded in the access log with your key as the actor. The reason is recorded against the redelivery; the budget is per API key.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
outboxId | string | no | The delivery to send again (`outboxId` in the delivery history). |
eventId | string | no | Instead of `outboxId`: the `ambition-event-id` your receiver saw. The newest delivery of that event to its receiver is sent again. |
endpointId | string | no | With `eventId`, when the event went to more than one of your receivers: which one to send it to again. |
reason | string | yes | Recorded against the redelivery, and shown when you read the delivery it creates. Never include patient data. |
Example response · 200
{
"data": {
"outboxId": "example_outboxId",
"requestId": "example_requestId",
"status": "example-status",
"replayOf": "example-replay-of",
"eventId": "example_eventId",
"endpointId": "example_endpointId",
"eventType": "example-event-type",
"created": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | not_found | No such delivery or event for this key's brand and environment. A sandbox key cannot address a production delivery. |
| 409 | not_replayable | Nothing to send again: a dead letter that was resolved or discarded, a reconciled delivery, a receiver's verification handshake, or a prescription transmission. |
| 409 | delivery_in_flight | The event is still being retried to this receiver. It arrives on its own schedule; read it with GET /deliveries/{id}. |
| 409 | receiver_not_active | The receiver the event went to is paused or disabled (a 410 disables it), or no longer subscribed to the event type. Resume the receiver in the console, or register another one, and catch up from GET /events, whose ids match `ambition-event-id`. |
| 409 | endpoint_required | The event went to more than one of your receivers: name one with `endpointId`. |
| 403 | phi_key_required | A clinical event is redelivered only with a phi_server key that holds `clinical_events.read`. |
| 409 | phi_gate_closed | A clinical event: your patient data access is not open now. Nothing was queued. |
| 409 | phi_suspended | A clinical event: your patient data access is suspended. Nothing was queued. |
| 409 | entity_baa_missing | A clinical event about a case: your BAA with the physician group covering it is not in force now. |
| 409 | clinical_event_held | A clinical event that is held (or expired) is released by Ambition, not redelivered. |
| 429 | replay_budget_exhausted | This API key has queued its redelivery budget for the current window (each key has its own). Repeating a queued redelivery costs nothing. |
| 422 | validation_error | Request body did not match the contract. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/pharmacy/callbacks/{partnerId}partner signatureReceive a signed pharmacy status callback
Partner-only machine endpoint. The raw body is authenticated with the pharmacy contract's versioned HMAC secret; timestamp and event headers are mandatory, replay-bounded, and matched to the body before an immutable receipt or order transition is recorded. A cancellation Ambition sent is a request until the pharmacy answers: `canceled` confirms it and `cancel_denied` (with a reason code) refuses it, leaving the order where it is. A shipped status may carry the carrier and tracking number, which the order records. The whole partner contract, including what Ambition sends the pharmacy, is published at GET /api/v1/pharmacy/order-api.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
eventId | string | yes | |
eventType | "prescription.status" | "prescription.clarification" | "fulfillment.status" | yes | |
schemaVersion | string | yes | |
orderId | string | yes | |
partnerReference | string | yes | |
status | "acknowledged" | "clarification_required" | "accepted" | "rejected" | "in_production" | "shipped" | "delivered" | "canceled" | "cancel_denied" | yes | |
occurredAt | string | yes | |
reasonCode | string | null | no | A code: letters, digits and _ . : - only, at most 120 characters. Anything else, up to 2000 characters, is recorded as a generic code (the status still applies), so explanations don't belong here. |
question | string | null | no | |
carrier | "ups" | "fedex" | "usps" | "dhl" | "other" | null | no | |
trackingNumber | string | null | no |
Example response · 202
{
"ok": false,
"outcome": "example-outcome"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | The callback body is not valid JSON. |
| 401 | invalid_callback_signature | The HMAC or timestamp window is invalid. |
| 404 | callback_contract_unavailable | No active callback contract matches the partner. |
| 422 | callback_validation_failed | The body, event identity, schema, time, or order tuple is invalid. A signed callback whose status is outside the canonical vocabulary is answered with code callback_status_unrecognized and kept whole in quarantine for an integration operator; it is never applied. |
| 429 | rate_limited | The public API callback budget is exhausted. |
| 503 | callback_store_unavailable | The durable callback store is unavailable. |
| 413 | payload_too_large | The request body exceeds 262144 bytes (256 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API, with the same status and body. |
/api/v1/sandbox/orderssessionCreate a deterministic synthetic order preview
Models the order path end to end against synthetic data. Nothing is prescribed, transmitted to a pharmacy, or charged. A console-session route: it needs a same-origin request from a signed-in tenant workforce member and does not accept an API key.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programKey | string | yes | The program the order belongs to, e.g. `hair-health`. |
sku | string | yes | The product SKU, uppercase. |
jurisdiction | string | yes | Two-letter US state code. |
visitMode | "async" | "video" | yes | How the clinician visit is held. |
Example response · 200
{
"data": {},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 403 | forbidden | Tenant integration access is required. |
| 409 | idempotency_conflict | Key reused with different parameters. |
| 409 | idempotency_in_progress | A request with this key is still being processed. Retry after `Retry-After`; nothing was applied twice. |
| 422 | validation_error | Request body did not match the contract. |
| 413 | payload_too_large | The request body exceeds 8192 bytes (8 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/sandbox/enrollments/{id}/simulateenrollments.writeSimulate a clinician's decision on a sandbox enrollment
Sandbox only. Stands in for the clinician who does not exist on the developer sandbox: moves one of your sandbox enrollments to `approved`, `needs_info`, `not_approved` or `canceled` in a single transition, so your receivers get the same `enrollment.status_changed` webhook (one per subscribed receiver) and `GET /v1/enrollments/{id}` the same status a real decision would produce. Nothing clinical is created: no case, clinician or prescription. Run the full test patient flow first: create the enrollment, open its `launchUrl` on your sandbox care host and verify a test email; then simulate the decision. `canceled` also works before that. Asking for the status the enrollment already holds changes nothing (`meta.changed: false`) and sends no webhook, so a retry is safe. Requires a sandbox secret key (`ak_sandbox_`) with `enrollments.write`; a live key is refused 403, and the database refuses any enrollment that is not synthetic sandbox data. Status is shown exactly as the enrollment read shows it: without your business associate agreement and downstream-systems attestation in force the decision reads `in_progress`, and no `enrollment.status_changed` is sent.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
outcome | "approved" | "needs_info" | "not_approved" | "canceled" | yes | The decision to simulate. `approved`, `needs_info` and `not_approved` need a test patient who has redeemed the enrollment's launch link on your sandbox care host; `canceled` works at any open stage. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"externalReference": "example-external-reference",
"status": "created",
"binding": "confirmed",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"outcome": "approved",
"changed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | invalid_json | The body is not valid JSON. |
| 403 | sandbox_key_required | The simulator runs only with a sandbox secret key, on the developer sandbox, against synthetic data. |
| 404 | enrollment_not_found | No enrollment of your brand in the sandbox has that id. |
| 409 | enrollment_not_bound | No test patient has redeemed this enrollment's launch link yet. Only `canceled` can be simulated before that. |
| 409 | simulation_not_allowed | The enrollment cannot move to that outcome from where it is, for example because it has closed. |
| 422 | validation_error | The body does not match the contract. |
| 503 | enrollment_unavailable | The enrollment record could not be reached. Nothing was changed; retry. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/programscatalog.readYour brand's catalog, for server callers
The same document as `GET /catalog/{brand}/programs`, for the key's own brand and environment. Use it from a backend that already holds a key; the public endpoint needs none. The answer is not cached.
Example response · 200
{
"data": {
"brand": {
"handle": "example-handle",
"name": "example-name"
},
"programs": [
{
"handle": "example-handle",
"name": "example-name",
"offer": {
"amountCents": 1,
"currency": "usd",
"interval": "week",
"trialDays": 1,
"display": "example-display",
"offerVersion": 1,
"termsHash": "example-terms-hash",
"effectiveTo": "example-effective-to"
},
"disclosure": {
"version": "example-version",
"digest": "example-digest"
}
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | brand_not_found | The key's brand has no catalog served on this deployment. |
| 503 | catalog_unavailable | The catalog could not be read. Nothing was changed; retry. |
/api/v1/enrollmentsenrollments.writeStart an enrollment and get a launch link to the care host
Creates an enrollment for your own customer in a program and state, and returns a single-use launch link to your brand's care host, where the person proves their email, consents, completes intake and saves a card. Name the program by its catalog handle (or its key) and the state the person will be cared for in; the same authority the care host applies decides whether it is offered there, so California, a state the program is not offered in, or one no licensed clinician covers is refused 422 `program_unavailable_in_state`. In production a program and state the physician network has not yet approved for enrollment is refused 422 `program_awaiting_network_approval` instead: that one is not recorded against the key, so the same request works once the approval is in force. `Idempotency-Key` is required: a retry with the same key and body returns the same enrollment with a freshly minted launch link (`meta.replayed: true`); the earlier link stays valid until it expires. The answer is identical whether or not the email has an account. Brand and environment always come from the key: a sandbox key creates and reads only sandbox enrollments of its own brand, and another brand's enrollment is indistinguishable from one that does not exist.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programHandle | string | no | The program's opaque public handle from the catalog (`prg_` and 24 hex characters). |
programKey | string | no | The program's key, as an alternative to `programHandle`. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | The two-letter USPS state (or DC) the person will be cared for in. California is never available. |
externalReference | string | yes | Your own reference for this enrollment attempt, such as your customer or order id: 1 to 128 letters, digits and `._:-`, starting with a letter or digit. Unique among your brand's open enrollments in the key's environment: it names one attempt, not a customer for life, so once an enrollment is `canceled` or `expired` the same reference can start a new one. For a second enrollment while the first is still open (another program for the same customer), use a different reference. It can never be an email address or free text. |
email | string | no | Your customer's email, if you already hold it as your own customer. Only a person who proves this address with a one-time code on the care host can redeem the launch link, so a forwarded link cannot bind someone else to your reference. Stored only as a hash. Ambition does not yet email the launch link: send the person to `launchUrl` yourself. The answer is identical whether or not the address has an account. |
returnUrl | string | no | Where the care host's optional "Return to <brand>" link points after card setup: an https URL on one of your brand's verified external sites in this environment, with no port, credentials or fragment. It is checked and recorded with the enrollment; the care host does not show the link yet. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"externalReference": "example-external-reference",
"status": "created",
"binding": "confirmed",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"launchUrl": "example-launch-url",
"launchExpiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 422 | program_not_found | No program of your brand has that handle or key here. |
| 422 | program_unavailable | The program has no approved offer, or your brand is not live. |
| 422 | program_self_enrollment_closed | Self-serve enrollment is off for this program (the default while enrollment is invite-only), so no launch link is issued. A program manager turns it on in the console. |
| 422 | program_unavailable_in_state | The program is not offered in that state. California never is. |
| 422 | program_awaiting_network_approval | Production only: enrollment in this program and state is awaiting network approval. The physician group's medical director has not yet approved production enrollment there, or the approval has ended. Nothing is wrong with the request and nothing was created; ask Ambition where the approval stands. Not stored against the Idempotency-Key: retry with the same key once it is approved. |
| 422 | environment_not_served | The key's environment does not serve enrollments. |
| 422 | return_url_unverified | `returnUrl` is not an https URL on one of your verified external sites in this environment. |
| 409 | external_reference_taken | An open enrollment of yours already uses this externalReference. Find it with `GET /enrollments?externalReference=` (the one that is not `canceled` or `expired`) and get a fresh link with `POST /enrollments/{id}/launch`. The reference is free again once that enrollment is canceled or expires. An enrollment that reads `in_progress` holds its reference for as long as it reads that way. |
| 409 | care_host_unavailable | Your brand has no active care host in this environment. |
| 409 | enrollment_closed | A replay named an enrollment that has since closed. Start a new one with a new key. |
| 409 | enrollment_link_unavailable | A replay named an enrollment whose intake has since started, so it has no new launch link. The person continues by signing in on your care host. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed. |
| 503 | launch_link_unavailable | The enrollment was created, but its launch link could not be issued. Retry with the same Idempotency-Key for its launch link. |
| 503 | enrollment_unavailable | The enrollment record could not be reached. Nothing was changed; retry. |
| 403 | enrollment_admission_closed | This deployment is not admitting patients yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Answered before the request body is read or an Idempotency-Key is reserved: nothing was created and no launch link was minted. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
/api/v1/enrollments/{id}/launchenrollments.writeGet a fresh launch link for an open enrollment
Mints a new single-use launch link to your brand's care host for one of your open enrollments created through the API, so you can send back a person who did not finish after the first link expired (links last 30 minutes, and Ambition does not email them) and after the create's Idempotency-Key window closed. It changes nothing about the enrollment: it does not rebind it, and only the person bound to it, or for an unredeemed enrollment the person who proves the email you supplied, can redeem the link. Earlier links stay valid until they expire. Takes no body and no Idempotency-Key: a retry mints another link to the same enrollment. A canceled or expired enrollment has no link (409 `enrollment_closed`); start a new one with `POST /enrollments`, which may carry the same `externalReference`. Once the person has started intake there is no new link either, whatever the enrollment's state (409 `enrollment_link_unavailable`, one answer for every state `GET` reads as `in_progress`): the person continues by signing in on your care host. Brand and environment always come from the key: a sandbox key creates and reads only sandbox enrollments of its own brand, and another brand's enrollment is indistinguishable from one that does not exist.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"launchUrl": "example-launch-url",
"launchExpiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | enrollment_not_found | No API enrollment of your brand in this environment has that id. |
| 409 | enrollment_closed | The enrollment was canceled or has expired, so it has no launch link. A key whose creator may read clinical states also gets this for one that was not approved. |
| 409 | enrollment_link_unavailable | The person has already started intake, so there is no new launch link; they continue by signing in on your care host. The same answer whatever the enrollment's state. |
| 409 | care_host_unavailable | Your brand has no active care host in this environment. |
| 503 | enrollment_unavailable | The enrollment record could not be reached. Nothing was changed; retry. |
| 403 | enrollment_admission_closed | This deployment is not admitting patients yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Answered before the request body is read or an Idempotency-Key is reserved: nothing was created and no launch link was minted. Never answered on the sandbox. |
/api/v1/enrollmentsenrollments.readList your enrollments by reference or change time
Your brand's enrollments in the key's environment, oldest change first, filtered by your `externalReference` and by `updatedSince`, with cursor pagination. Poll with `updatedSince` set to the last `updatedAt` you saw. Status is coarse and treated as PHI: the intake and decision states are shown only with your brand's business associate agreement and downstream-systems attestation in force and minimum-necessary patient access held by the key's creator; otherwise they read `in_progress`. No answer carries a patient, a program, an email, an intake answer or a clinical reason. Every read is recorded as an access event. Brand and environment always come from the key: a sandbox key creates and reads only sandbox enrollments of its own brand, and another brand's enrollment is indistinguishable from one that does not exist.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
externalReference | string | no | Only enrollments with this reference of yours: at most one that is open, and any earlier attempts with the same reference that have closed. |
updatedSince | string | no | Only enrollments whose `updatedAt` is at or after this instant (ISO 8601). |
cursor | string | no | The `nextCursor` of the previous page. |
limit | integer | no | Per page, 1 to 100. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"externalReference": "example-external-reference",
"status": "created",
"binding": "confirmed",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"hasMore": false,
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Query parameters, or the cursor, were invalid. |
| 503 | enrollment_unavailable | The enrollment record could not be reached. Nothing was changed; retry. |
/api/v1/enrollments/{id}enrollments.readRead one enrollment's coarse status
One enrollment of your brand in the key's environment. Status is coarse and treated as PHI: the intake and decision states are shown only with your brand's business associate agreement and downstream-systems attestation in force and minimum-necessary patient access held by the key's creator; otherwise they read `in_progress`. No answer carries a patient, a program, an email, an intake answer or a clinical reason. Every read is recorded as an access event. Brand and environment always come from the key: a sandbox key creates and reads only sandbox enrollments of its own brand, and another brand's enrollment is indistinguishable from one that does not exist.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"externalReference": "example-external-reference",
"status": "created",
"binding": "confirmed",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 404 | enrollment_not_found | No enrollment of your brand in this environment has that id. |
| 503 | enrollment_unavailable | The enrollment record could not be reached. Nothing was changed; retry. |
/api/v1/eventsevents.readBackfill the webhook events of your brand
Events in the order Ambition recorded them, after an opaque cursor: catch a receiver up after downtime or seed a new one. `tier=standard` (default, `events.read`) lists commerce and enrollment events and never names a patient. `tier=clinical` needs `clinical_events.read` on a phi_server key and lists the refs-only `clinical.*` events of your API patients under the delivery rules: your patient data access open and unsuspended and, for a case event, your BAA covering the case's physician group now. A held event appears once it is released. Each event's `id` equals the `ambition-event-id` of its deliveries. Clinical pages are recorded in the access log with your key as the actor.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
tier | "standard" | "clinical" | no | Which tier to list. `clinical` needs `clinical_events.read` on a phi_server key. |
type | string | no | Only events of this type (it must be on the tier you list). |
patientId | string | no | Clinical tier only: one of your API patients. |
after | string | no | The `nextCursor` of the previous page. |
limit | integer | no | Events per page, 1 to 100. |
Example response · 200
{
"data": [
{
"id": "example_id",
"type": "example-type",
"livemode": false,
"schemaVersion": "example-schema-version",
"data": {},
"recordedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"tier": "standard",
"hasMore": false,
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 403 | clinical_events_scope_required | The clinical tier needs `clinical_events.read` on a phi_server key. |
| 403 | clinical_events_unavailable | Your patient data access is not open (not approved, withdrawn or suspended). |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 422 | validation_error | Query parameters were invalid (an unknown tier, a type of the other tier, a patient filter on the standard tier, or a malformed cursor). |
| 503 | events_unavailable | Events could not be read. Nothing was changed; retry. |
/api/v1/phi-access/statusWhether your patient data access is enabled, item by item
The patient data access checklist for the key's brand on this deployment's plane: the business associate agreement with the physician group, the privacy officer's frontend attestation, platform PHI terms (and merchant-of-record terms when a headless program bills with you as merchant), the commerce-terms and no-research-use-only commitments, the downstream vendor inventory, on a real-data deployment a verified sending domain (a coarse reason, never the domain itself), and the network approval; plus the kill switch, launch controls and the latest PHI page scan, and whether this deployment is admitting patients yet (`admittingPatients`). Any phi_server key may read it; a standard key gets 403 `phi_key_required`. Contains no patient data and no names.
Example response · 200
{
"data": {
"environment": "sandbox",
"enabled": false,
"phiAccess": "full",
"admittingPatients": false,
"suspended": false,
"completeCount": 1,
"requiredCount": 1,
"items": [
{
"key": "baa",
"state": "complete",
"reason": "example-reason",
"detail": {}
}
],
"launchControls": {
"required": false,
"missing": [
"example-missing"
]
},
"scan": {
"state": "none",
"ranAt": "2026-01-01T00:00:00.000Z",
"findings": 1,
"cureEndsAt": "2026-01-01T00:00:00.000Z"
},
"evaluatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 403 | phi_key_required | Only a phi_server key reads the PHI gate status. |
| 503 | phi_status_unavailable | The gate could not be read. Nothing was changed; retry. |
/api/v1/patientspatients.writeCreate a patient (your account is their identity)
Creates a delegated patient for your brand: an Ambition patient record bound to your externalId, with no Ambition login (a synthetic, non-routable identity). The same person in two brands is two patients. The email and phone are recorded as brand-asserted and never count as verified; demographics, when sent, are brand-asserted until identity verification matches them. 201 with the patient; 409 `patient_exists` with the existing id when the externalId or email is already your patient; 409 `hosted_patient_requires_link` when the email belongs to a patient who signed up on your care host. Needs Idempotency-Key and the patient headers. `Ambition-Patient-Auth: otp` is refused (401 `patient_sign_in_unverified`): a patient being created has no Ambition sign-in session; send the method your own sign-up used. A replay (the same Idempotency-Key and body) answers 201 with `meta.replayed` and only the patient's `id`, `externalId`, `status` and `revision`: no demographics are kept with the key. Read the rest with GET /patients/{patientId}.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string | yes | Your own account id for the patient: letters, digits and ._:- only, at most 128 (never an email address). |
email | string | yes | The address the patient gave you. Recorded as brand-asserted; verify it with a contact verification. |
phone | string | no | A US phone number; stored as +1 and ten digits. |
legalName | string | no | |
dateOfBirth | string | no | YYYY-MM-DD. |
shippingAddress | object | no | |
residenceState | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | no | Where the patient lives. Defaults to the shipping address's state. |
linkExisting | object | no | Not available yet: a hosted patient links only by proving control of their own account (after launch). Answers 409. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"externalId": "example_externalId",
"status": "active",
"revision": 1,
"residenceState": "AL",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"demographics": {
"legalName": "example-legal-name",
"dateOfBirth": "example-date-of-birth",
"phone": "example-phone",
"shippingAddress": {
"line1": "example-line1",
"line2": "example-line2",
"city": "example-city",
"state": "AL",
"postalCode": "example-postal-code",
"country": "US"
},
"source": "brand_asserted",
"version": 1
},
"contactPoints": [
{
"kind": "email",
"value": "example-value",
"verifiedBy": "brand_asserted",
"verifiedAt": "2026-01-01T00:00:00.000Z"
}
],
"contactVerified": false,
"identityVerified": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 403 | phi_access_not_enabled | Your brand's PHI gate is closed, suspended, or the key's owner lost the capability. |
| 409 | patient_exists | That externalId or email is already your patient; `error.resource_id` (and `Location`) names its id. |
| 409 | hosted_patient_requires_link | The email belongs to a hosted patient; linking needs the patient's own proof (after launch). |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent. A patient being created has no Ambition sign-in session, so otp cannot be claimed here; send the method your own sign-up used. Nothing was created and the Idempotency-Key is not spent. |
| 413 | payload_too_large | The request body exceeds 8192 bytes (8 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
/api/v1/patientspatients.readFind your patient by externalId
Your active patient with that externalId, as a one-element list, or an empty list. Exact match only. With `Ambition-Patient-Auth: otp`, `Ambition-Patient-Session` must be a current Ambition sign-in session reference of the patient found, from this key; otherwise 401 `patient_sign_in_unverified`.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string | yes | Your own account id for the patient: letters, digits and ._:- only, at most 128 (never an email address). |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"externalId": "example_externalId",
"status": "active",
"revision": 1
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 403 | phi_access_not_enabled | Your brand's PHI gate is closed. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}patients.readRead a patient
The patient's record as your brand may see it: reference, demographics, contact points with who verified each, and verification state. Audited.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"externalId": "example_externalId",
"status": "active",
"revision": 1,
"residenceState": "AL",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"demographics": {
"legalName": "example-legal-name",
"dateOfBirth": "example-date-of-birth",
"phone": "example-phone",
"shippingAddress": {
"line1": "example-line1",
"line2": "example-line2",
"city": "example-city",
"state": "AL",
"postalCode": "example-postal-code",
"country": "US"
},
"source": "brand_asserted",
"version": 1
},
"contactPoints": [
{
"kind": "email",
"value": "example-value",
"verifiedBy": "brand_asserted",
"verifiedAt": "2026-01-01T00:00:00.000Z"
}
],
"contactVerified": false,
"identityVerified": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}patients.writeUpdate contact, demographics or shipping address
Send `If-Match: <revision>`. A changed email or phone is brand-asserted again and needs a new contact verification. When an Ambition-verified email is replaced, Ambition sends the previous address a neutral notice, asynchronously, with no new address, no link and nothing about care; a brand-asserted email gets none. The legal name and date of birth change only while they are your assertion; once identity verification matched them, or while a prescription is in progress, 409 `identity_fields_locked` (an amendment the care team reviews).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | no | |
phone | string | no | A US phone number; stored as +1 and ten digits. |
legalName | string | no | |
dateOfBirth | string | no | YYYY-MM-DD. |
shippingAddress | object | no |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"externalId": "example_externalId",
"status": "active",
"revision": 1,
"residenceState": "AL",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"demographics": {
"legalName": "example-legal-name",
"dateOfBirth": "example-date-of-birth",
"phone": "example-phone",
"shippingAddress": {
"line1": "example-line1",
"line2": "example-line2",
"city": "example-city",
"state": "AL",
"postalCode": "example-postal-code",
"country": "US"
},
"source": "brand_asserted",
"version": 1
},
"contactPoints": [
{
"kind": "email",
"value": "example-value",
"verifiedBy": "brand_asserted",
"verifiedAt": "2026-01-01T00:00:00.000Z"
}
],
"contactVerified": false,
"identityVerified": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 428 | if_match_required | Send the patient's revision as If-Match. |
| 412 | revision_mismatch | The patient changed since you read it. |
| 409 | identity_fields_locked | The legal name and date of birth are verified or a prescription is in progress. |
| 409 | email_in_use | Another of your patients has that email. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 8192 bytes (8 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/deactivatepatients.writeClose the patient's API access
The record stays with the physician group; the patient can still reach it through the Ambition records request. Refused while a prescription is in progress. Later calls for the patient answer 404.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
reason | "brand_request" | "patient_request" | "duplicate" | "other" | no |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"status": "deactivated"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 409 | patient_has_active_care | A prescription is in progress. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/contact-verificationspatients.writeSend an Ambition contact code
Ambition generates a six-digit code and sends it to the patient's email from your verified domain (or the physician group's sender). You never see the code: relay what the patient types to `/confirm`. Use it as your sign-up code; it proves the patient controls that email, but it is not an Ambition sign-in: afterwards send the method your own sign-in used, not `otp` (`otp` needs an Ambition sign-in link's sessionReference). Required once before the first prescription is transmitted (hold `contact_unverified`). Ten minutes, five attempts, five codes per patient per hour. On the sandbox the email is caught, never delivered: read the code with `GET /sandbox/patients/{patientId}/messages`.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
channel | "email" | "sms" | yes | `email` at launch; `sms` answers 422 `sms_not_available` until 10DLC registration. |
Example response · 201
{
"data": {
"verificationId": "00000000-0000-4000-8000-00000000e001",
"status": "sent",
"channel": "email",
"destinationHint": "example-destination-hint",
"expiresAt": "2026-01-01T00:00:00.000Z",
"attemptsRemaining": 1
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 422 | sms_not_available | SMS codes wait on 10DLC registration; use email. |
| 429 | contact_verification_rate_limited | Too many codes for this patient this hour. |
| 503 | contact_sender_unavailable | No sender is configured; the code was not sent. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/contact-verifications/{verificationId}/confirmpatients.writeConfirm the code the patient typed
`verified`, or `invalid_code` (with attempts remaining), `expired`, `locked` or `undelivered`. A wrong code counts as an attempt.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
code | string | yes | The six-digit code the patient received and typed into your UI. |
Example response · 200
{
"data": {
"verificationId": "00000000-0000-4000-8000-00000000e001",
"status": "sent",
"channel": "email",
"destinationHint": "example-destination-hint",
"expiresAt": "2026-01-01T00:00:00.000Z",
"attemptsRemaining": 1
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 404 | verification_not_found | No verification with that id for this patient. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/identity-verificationsidentity.writeVerify identity (BYO vendor or Ambition-run)
`{mode: byo, vendor, inquiryId}`: you ran the inquiry at your allowlisted vendor with reference_id = the patientId; Ambition fetches the result with the read-only credential you registered and checks the binding, that the inquiry was created after the patient, the template (government photo ID and selfie liveness), that the inquiry was never used for another patient, and that the verified name and date of birth match the record. `{mode: ambition}`: Ambition creates the inquiry and returns the client token for the vendor SDK you embed; poll `/current`. Your assertion of identity never counts. Identity is needed before the case enters the clinician queue.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
mode | "byo" | "ambition" | yes | `byo`: your vendor's inquiry. `ambition`: Ambition creates the inquiry for you to embed. |
vendor | "persona" | "vouched" | "incode" | "sandbox_fixture" | no | byo only: your allowlisted vendor (`sandbox_fixture` on the sandbox only). |
inquiryId | string | no | byo only: the inquiry you created with reference_id = the Ambition patientId. |
Example response · 201
{
"data": {
"verificationId": "00000000-0000-4000-8000-00000000e001",
"status": "not_started",
"method": "byo_vendor",
"vendor": "example-vendor",
"synthetic": false,
"verifiedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"reasonCode": "example-reason-code",
"clientToken": "example-client-token",
"sessionUrl": "example-session-url",
"sessionExpiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 409 | inquiry_already_used | That inquiry was already submitted (for this or another patient). |
| 409 | demographics_required | Record the legal name and date of birth first. |
| 409 | identity_review_open | An earlier check for this patient is waiting on a person: the covering physician group reviews it and you learn the outcome from `clinical.identity_verification.completed` or `.failed`. Retry only after that; a new check cannot replace an open review. |
| 422 | idv_vendor_not_allowed | The vendor is not on the allowlist. |
| 422 | idv_credential_missing | Register a read-only credential for the vendor first. |
| 503 | idv_vendor_not_certified | Ambition has not certified this vendor's adapter yet; the inquiry stays pending. |
| 503 | idv_vendor_unavailable | The vendor could not be reached; the inquiry stays pending. Poll `/current`. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/identity-verifications/currentpatients.readThe patient's identity verification state
A pending inquiry is fetched from the vendor at most every 10 seconds, so polling this settles it. Coarse reason codes only; nothing the vendor saw.
Example response · 200
{
"data": {
"verificationId": "00000000-0000-4000-8000-00000000e001",
"status": "not_started",
"method": "byo_vendor",
"vendor": "example-vendor",
"synthetic": false,
"verifiedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"reasonCode": "example-reason-code",
"clientToken": "example-client-token",
"sessionUrl": "example-session-url",
"sessionExpiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/eligibilityeligibility.checkCan this patient be admitted to the program in a state
Reason codes only (never a clinician or licence). Also says what stands between the patient and a prescription: consent scopes, identity, an Ambition-verified contact. Headless programs serve async states at launch: a state that needs a video visit answers `modality_unavailable_headless`. California is excluded. While the program's self-serve enrollment is off (the default for the invite-only pilot) every state answers `program_self_enrollment_closed`. Nothing is written but the audit.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programHandle | string | yes | The program's opaque public handle (`prg_` and 24 hex characters). Link the care host's Start page with it: `/start?p=<handle>`. It never changes and carries no program or condition word. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes |
Example response · 200
{
"data": {
"programHandle": "example-program-handle",
"state": "AL",
"eligible": false,
"reasonCode": "example-reason-code",
"requiredModality": "async",
"requiredConsentScopes": [
"example-required-consent-scopes"
],
"consentCaptureMode": "brand_rendered",
"identityRequired": false,
"identityVerificationMode": "byo_vendor",
"contactVerificationRequired": false,
"paymentRequired": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 404 | program_not_found | No program with that handle in your brand. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/enrollmentsintake.writeAdmit the patient to a program
Headless admission: no launch token, no OTP. The enrollment authority rule and eligibility apply; a contact need not be verified yet (that gates the first transmission). The program's self-serve enrollment must be on, as for `POST /enrollments`. One open enrollment per program and state: a repeat returns it (200), even after the switch is turned off.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programHandle | string | yes | The program's opaque public handle (`prg_` and 24 hex characters). Link the care host's Start page with it: `/start?p=<handle>`. It never changes and carries no program or condition word. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "example-status",
"caseId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 422 | not_eligible | The patient cannot be admitted; `code` is the eligibility reason. |
| 422 | program_self_enrollment_closed | Self-serve enrollment is off for this program (the default while enrollment is invite-only), so no new patient is admitted. A program manager turns it on in the console once your physician group approves self-serve enrollment. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/enrollmentspatients.readThe patient's enrollments
Every enrollment of the patient in your brand, newest first, with coarse status.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "example-status",
"caseId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/documentsdocuments.readThe patient's documents
Consent records (with the exact text accepted), care records, signed prescriptions and lab reports the clinician signed off; every document is offered as html, json and pdf. A case-bound document is listed only where your BAA covers the case. Audited. `clinical.document.available` announces each new one.
Example response · 200
{
"data": [
{
"id": "example_id",
"kind": "consent_record",
"title": "example-title",
"status": "example-status",
"createdAt": "2026-01-01T00:00:00.000Z",
"caseId": "00000000-0000-4000-8000-00000000e001",
"formats": [
"html"
]
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/documents/{documentId}/contentdocuments.readOne document's content
`format=json` (default), `html` (escaped, no scripts) or `pdf` (rendered on request from the same minimum-necessary content, headed by your brand's mark; `Content-Disposition: attachment` with a name that is the kind of document only, never the patient's). Each read, in any format, is one audited read; never cached. A case-bound document (care record, prescription, lab report) needs your BAA with the physician group covering that case.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
format | "json" | "html" | "pdf" | no | `json` (default), `html` (escaped, no scripts) or `pdf` (rendered on request, headed by your brand's mark). |
Example response · 200
{
"data": {},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 404 | document_not_found | No document with that id for this patient. |
| 403 | entity_baa_missing | A care record, prescription or lab report whose case's physician group your BAA does not cover. |
| 503 | document_render_unavailable | The PDF could not be rendered. Nothing about the document is in the error; retry or read `html`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/record-exportsdocuments.readExport the patient's record (right of access)
The patient's copy of their record, logged as a fulfilled right-of-access request. The content is in this answer only; store it. A replay (the same Idempotency-Key) answers 201 with the same export's manifest (every field the first answer had, the same values), `content` null and `meta.replayed` true: the record is never exported twice. Cases (and their prescriptions) whose physician group your BAA does not cover are left out; the patient obtains those through the group's records request.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"status": "example-status",
"schema": "ambition.patient-record-export.v1",
"generatedAt": "2026-01-01T00:00:00.000Z",
"createdAt": "2026-01-01T00:00:00.000Z",
"sha256": "example-sha256",
"sections": {},
"content": {}
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. |
| 422 | validation_error | The body was invalid. |
| 503 | patient_service_unavailable | The record could not be exported, or the export read again. Nothing was exported twice; retry. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/record-exports/{exportId}documents.readAn export's manifest
The digest and section counts of an earlier export (no content).
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"status": "example-status",
"schema": "ambition.patient-record-export.v1",
"generatedAt": "2026-01-01T00:00:00.000Z",
"createdAt": "2026-01-01T00:00:00.000Z",
"sha256": "example-sha256",
"sections": {},
"content": {}
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key (also: another brand's patient, PHI gate closed, key suspended). |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 404 | export_not_found | No export with that id for this patient. |
| 503 | patient_service_unavailable | The record could not be exported, or the export read again. Nothing was exported twice; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/referralsintake.writeRefer the patient to your medical group
Refers your patient to the medical group that covers your brand, for one program and state. On live data this is how a patient you created becomes eligible for intake. Every eligibility rule applies except the program's self-serve switch (a referral the group reviews is not self-serve). The referral waits for the group (`pending_group_review`; `clinical.referral.accepted` or `.declined` follows, `.expired` after 30 days with no decision) unless the group's written policy allows self-enrollment and the program's self-serve switch is on, when it is accepted at once (`acceptance: group_policy`). One open referral per program and state: a repeat returns it (200).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programHandle | string | yes | The program's opaque public handle from the catalog (`prg_` and 24 hex characters). |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | The two-letter USPS state (or DC) the patient will be cared for in. California is never available. |
externalReference | string | no | Your own reference for this referral (an order or request id): letters, digits and `._:-`. Never PHI. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "pending_group_review",
"acceptance": "group_review",
"sandboxDecision": false,
"declineReason": "outside_group_scope",
"externalReference": "example-external-reference",
"enrollmentId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"decidedAt": "2026-01-01T00:00:00.000Z",
"enrolledAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 404 | program_not_found | No program with that handle in your brand. |
| 422 | program_not_headless | The program does not run headless or hybrid. |
| 422 | state_excluded | California is never available. |
| 422 | modality_unavailable_headless | The state needs a video visit, which headless care does not serve yet. |
| 422 | no_clinician_coverage | No clinician of a covering group is licensed in the state. |
| 422 | enrollment_authorization_required | Live data only: the medical group's medical director has not authorized enrollment for this program and state. Any other enrollment-authority or availability reason is answered as its own code. |
| 422 | medical_group_unavailable | No verified medical group serves your brand and this program. |
| 422 | referrals_not_accepted | The medical group is not taking referrals from your brand now. |
| 422 | external_reference_invalid | externalReference is 1 to 128 letters, digits and `._:-`. |
| 409 | referral_declined | The medical group declined this patient for this program and state in the last 30 days. A later referral always waits for the group's review. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/referralspatients.readThe patient's referrals
Every referral of the patient in your brand, newest first. Audited.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "pending_group_review",
"acceptance": "group_review",
"sandboxDecision": false,
"declineReason": "outside_group_scope",
"externalReference": "example-external-reference",
"enrollmentId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"decidedAt": "2026-01-01T00:00:00.000Z",
"enrolledAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/referrals/{referralId}patients.readOne referral
One referral of the patient, as it is now (an open referral past its expiry reads `expired`). Audited.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "pending_group_review",
"acceptance": "group_review",
"sandboxDecision": false,
"declineReason": "outside_group_scope",
"externalReference": "example-external-reference",
"enrollmentId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"decidedAt": "2026-01-01T00:00:00.000Z",
"enrolledAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | referral_not_found | No referral with that id for this patient. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/referrals/{referralId}/acceptintake.writeThe patient accepts the referral
Call from the patient's signed-in session in your app, once the group accepted the referral, the patient accepted every consent the program requires (consent presentations and consents) and you showed the program's terms. Ambition checks the consent evidence, the terms digest and the enrollment authority again, then enrolls the patient (`status: enrolled`, `enrollmentId`): intake drafts and submissions for the program and state are accepted from then on. Identity verification is not needed here; a case without it waits in `awaiting_identity`. A repeat returns the enrolled referral.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
termsDigest | string | yes | The `disclosure.digest` of the program terms you showed the patient (GET /catalog/{brand}/programs/{handle}). It records your attestation that the patient was shown these terms; Ambition checks they are the program's current approved terms (if they changed, show the new ones and send their digest). Consent evidence, by contrast, is Ambition's own (presentation nonces and render receipts). |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "pending_group_review",
"acceptance": "group_review",
"sandboxDecision": false,
"declineReason": "outside_group_scope",
"externalReference": "example-external-reference",
"enrollmentId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"decidedAt": "2026-01-01T00:00:00.000Z",
"enrolledAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 404 | referral_not_found | No referral with that id for this patient. |
| 409 | referral_pending_group_review | The medical group has not accepted the referral yet, or it was accepted by the group's policy and that policy (or the program's self-serve switch) has changed since, so it went back to the group's review. |
| 422 | program_not_headless | The program no longer runs headless or hybrid. |
| 422 | modality_unavailable_headless | The state now needs a video visit, which headless care does not serve yet. |
| 422 | no_clinician_coverage | No clinician of a covering group is licensed in the state any more. Every eligibility and enrollment-authority reason is checked again at acceptance and answered as its own code. |
| 409 | referral_declined | The medical group declined the referral. |
| 409 | referral_withdrawn | You withdrew the referral. |
| 409 | referral_expired | The referral expired; make a new one. |
| 422 | consent_required | The patient has not accepted the current consent document of every scope the program requires in the state. |
| 422 | reconsent_required | A consent document changed since the patient accepted it. |
| 422 | hosted_consent_required | A required consent must be captured on the hosted component. |
| 422 | terms_acceptance_required | Send termsDigest. |
| 422 | terms_digest_invalid | termsDigest is a lowercase hex SHA-256. |
| 409 | terms_changed | The program terms changed; show the current terms and send their digest. |
| 422 | program_terms_unavailable | The program has no approved terms. |
| 422 | enrollment_authorization_cap_reached | The group's authorization admits no more patients. |
| 422 | enrollment_authorization_required | Live data only: enrollment for this program and state is not authorized. |
| 422 | medical_group_unavailable | The medical group no longer serves your brand and this program. |
| 422 | referrals_not_accepted | The medical group is not taking referrals from your brand now. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/referrals/{referralId}/withdrawintake.writeWithdraw an open referral
Closes a referral that is still waiting for the group or for the patient. A referral the patient accepted cannot be withdrawn: cancel the case instead. A repeat returns the withdrawn referral.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "pending_group_review",
"acceptance": "group_review",
"sandboxDecision": false,
"declineReason": "outside_group_scope",
"externalReference": "example-external-reference",
"enrollmentId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"decidedAt": "2026-01-01T00:00:00.000Z",
"enrolledAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | referral_not_found | No referral with that id for this patient. |
| 409 | referral_enrolled | The patient already accepted; cancel their case instead. |
| 409 | referral_declined | The medical group declined the referral. |
| 409 | referral_expired | The referral already expired. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/patients/{patientId}/referrals/{referralId}/simulateintake.writeSandbox: the medical group accepts or declines a referral
The sandbox medical group decides a referral waiting for review, through the same decision the group's own members make (`clinical.referral.accepted` or `.declined`, livemode false; `sandboxDecision: true` on the referral). Sandbox only: a sandbox phi_server key, a synthetic brand and the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). 600 decisions per brand per hour. The same decision again answers the referral as it is.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
decision | "accept" | "decline" | yes | What the sandbox medical group decides. |
reason | "outside_group_scope" | "group_capacity" | "duplicate_referral" | "information_incomplete" | "other" | no | Required to decline; not sent to accept. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"state": "AL",
"status": "pending_group_review",
"acceptance": "group_review",
"sandboxDecision": false,
"declineReason": "outside_group_scope",
"externalReference": "example-external-reference",
"enrollmentId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"decidedAt": "2026-01-01T00:00:00.000Z",
"enrolledAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 404 | referral_not_found | No referral with that id for this patient. |
| 403 | sandbox_simulation_only | Developer sandbox, sandbox key and synthetic brand only. |
| 409 | referral_not_pending | The referral is not waiting for the medical group. |
| 409 | referral_expired | The referral expired before a decision. |
| 422 | referral_decision_invalid | decision is accept or decline; an acceptance takes no reason. |
| 422 | referral_decline_reason_invalid | A decline names one reason code. |
| 422 | medical_group_unavailable | The medical group no longer serves your brand and this program. |
| 429 | sandbox_simulation_rate_limited | At most 600 sandbox decisions per brand per hour. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sign-in-linkspatients.writeEmail the patient a one-time sign-in link to your site
Emails your patient's Ambition-verified address a one-time link to a verified, attested hand-off page of your own site: `<redirectUrl>#ambition_sign_in=<signInLinkId>.<token>` (no query), sent from your brand's sender and showing only your brand's name, the link, its expiry and what to do if they did not ask. It names no program, condition or medication. The link works once, for 15 minutes, and allows 5 confirmation attempts; at most 5 links per patient in 15 minutes. The answer never carries the token or the address. Your hand-off page reads the fragment, clears it (`history.replaceState`), loads no third-party script, and posts it to your server when the patient clicks to continue (never automatically on load: a link scanner that runs scripts would spend the link); your server then calls confirmSignInLink. Takes no patient headers: the patient has not signed in to you yet. Not refused while the deployment admits no new patients: it reaches only patients who already hold an Ambition-verified email. Needs Idempotency-Key; a replay answers the same link and sends nothing.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
patientId | string | yes | Your delegated patient's Ambition id. They need an Ambition-verified email. |
redirectUrl | string | yes | Where the link lands: an https URL of at most 2048 characters, with no query, port, credentials or fragment, whose origin is a verified external site of your brand in this environment with an in-force site attestation, and whose path is a page you registered under that site as a hand-off page (the console's External sites page). The emailed link is exactly that origin and path plus the fragment; keep any state (where to go next) in your own first-party session, not in the URL. |
Example response · 201
{
"data": {
"signInLinkId": "00000000-0000-4000-8000-00000000e001",
"status": "sent",
"destinationHint": "example-destination-hint",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 429 | rate_limited | Too many requests, per network address and per API key, or (opening a link) more than 5 links for this patient in 15 minutes, in which case nothing is sent. Retry after the interval in `Retry-After` (the patient's budget frees as their links age). |
| 403 | browser_origin_refused | Sign-in links are server-to-server only: call from your backend, never from a browser. |
| 403 | phi_key_required | Sign-in links take a phi_server key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed; retry. |
| 404 | patient_not_found | No patient with that id is available to this key: another brand's or environment's patient, a deactivated patient, or the PHI gate closed or suspended. |
| 409 | contact_not_verified | The patient has no Ambition-verified email. Verify it first (POST /patients/{patientId}/contact-verifications). Nothing was sent. |
| 422 | redirect_url_query_not_allowed | redirectUrl carries a query string. The emailed link is exactly your registered hand-off page and the fragment; keep any state in your own first-party session. Nothing was sent. |
| 422 | redirect_url_unverified | redirectUrl is not an https URL of at most 2048 characters with no port, credentials or fragment on a verified external site of your brand in this environment that holds an in-force site attestation, or its path is not a live page of that site registered as a hand-off page. Nothing was sent. |
| 503 | contact_sender_unavailable | The email could not be sent, or its send could not be recorded. Nothing usable was issued (a link whose send is not recorded can never sign anyone in) and the Idempotency-Key is not spent; retry. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
/api/v1/sign-in-links/{signInLinkId}/confirmpatients.writeConfirm a sign-in link's token
Your server sends the token your hand-off page read from the link's fragment. `signed_in` (once) names the patient and answers a `sessionReference`: sign them in on your side, and on the delegated calls you make for that sign-in send the reference as `Ambition-Patient-Session` with `Ambition-Patient-Auth: otp`, because the link was opened by whoever controls the patient's current Ambition-verified email (which your brand sets: this proves control of that email, not the patient's identity). Ambition accepts an otp claim only with a current session reference (24 hours, the patient's, from the key that confirmed the link, not signed out, the email not replaced since); anything else is 401 patient_sign_in_unverified. Sign out with POST /sign-in-sessions/revoke. A wrong token counts an attempt (`invalid`, with attemptsRemaining; the 5th locks the link); `used`, `expired`, `revoked` and `locked` links say so, and sign no one in. The hand-off page is checked again here: a link whose page was removed, whose site is no longer verified or whose site attestation is no longer in force is `revoked`. A link of another brand or environment is not found. Takes no patient headers. Needs Idempotency-Key; a replay answers the first confirmation's outcome and counts nothing (a signed_in replay answers the same sessionReference).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | yes | The token from the link's fragment: the part after the dot (`sil_` and 43 characters). |
Example response · 200
{
"data": {
"signInLinkId": "00000000-0000-4000-8000-00000000e001",
"status": "signed_in",
"patientId": "00000000-0000-4000-8000-00000000e001",
"authMethod": "otp",
"signedInAt": "2026-01-01T00:00:00.000Z",
"sessionReference": "example-session-reference",
"sessionExpiresAt": "2026-01-01T00:00:00.000Z",
"attemptsRemaining": 1
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 429 | rate_limited | Too many requests, per network address and per API key, or (opening a link) more than 5 links for this patient in 15 minutes, in which case nothing is sent. Retry after the interval in `Retry-After` (the patient's budget frees as their links age). |
| 403 | browser_origin_refused | Sign-in links are server-to-server only: call from your backend, never from a browser. |
| 403 | phi_key_required | Sign-in links take a phi_server key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed; retry. |
| 404 | sign_in_link_not_found | No sign-in link with that id for your brand in this environment (also: the PHI gate closed or suspended). |
| 503 | sign_in_session_unavailable | The token was right and the link is spent, but its Ambition sign-in session could not be recorded. Retry this confirmation with the same Idempotency-Key and token within 10 minutes: the retry records the session and answers signed_in with the sessionReference. |
| 409 | sign_in_session_not_bound | A replay of a signed_in confirmation whose sign-in session can no longer back otp: it was not recorded within 10 minutes of the confirmation, another key of your brand confirmed the link, the patient's email was replaced, or the session was signed out or is past its 24 hours. Ask for a new sign-in link. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
/api/v1/sign-in-sessions/revokepatients.writeSign the patient out of an Ambition sign-in session
Call it when the patient signs out of your site (and whenever you end that session): the sessionReference stops backing `Ambition-Patient-Auth: otp` at once, and a replay of its confirmation answers 409 sign_in_session_not_bound instead of the reference. Revokes the session of this patient, bound by this key; another brand's, key's or patient's session, or an unknown one, is 404 sign_in_session_not_found. Signing out again answers `revoked` again. Ending access is never refused for your brand's state: it works with the PHI gate closed or the brand suspended, needing only a live phi_server key with patients.write. Takes no patient headers. Needs Idempotency-Key.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
patientId | string | yes | The patient the sign-in session is for. |
sessionReference | string | yes | The sessionReference the confirmation answered (`ass_` and 43 characters). Only its digest reaches the database. |
Example response · 200
{
"data": {
"status": "revoked"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 429 | rate_limited | Too many requests, per network address and per API key, or (opening a link) more than 5 links for this patient in 15 minutes, in which case nothing is sent. Retry after the interval in `Retry-After` (the patient's budget frees as their links age). |
| 403 | browser_origin_refused | Sign-in sessions are server-to-server only: call from your backend, never from a browser. |
| 403 | phi_key_required | Sign-in sessions take a phi_server key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed; retry. |
| 404 | sign_in_session_not_found | No sign-in session of this patient with that reference was bound by this key (also: another brand's, key's or patient's session). Never answered for a closed PHI gate or a suspension: those do not stop a sign-out. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
/api/v1/patients/{patientId}/consent-presentationsconsents.writeIssue presentation nonces for the consent documents you will show
One single-use presentation per document, bound to the patient, the document version, its text and label digests, the program, state and locale, valid 60 minutes. Refused with `hosted_consent_required` for a scope that must be captured on the hosted component, and `document_not_current` for a document that is not the current text.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programHandle | string | yes | The program's opaque public handle (`prg_` and 24 hex characters). Link the care host's Start page with it: `/start?p=<handle>`. It never changes and carries no program or condition word. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | The state the patient is in; the documents are the ones issued for it. |
locale | string | no | A BCP 47 language-region tag, e.g. `en-US`. |
documentIds | array | yes | The `documentId`s you will show, one per scope. |
Example response · 201
{
"data": [
{
"presentationId": "00000000-0000-4000-8000-00000000e001",
"documentId": "00000000-0000-4000-8000-00000000e001",
"scope": "telehealth_consent",
"version": 1,
"sha256": "example-sha256",
"labelSha256": "example-label-sha256",
"locale": "example-locale",
"issuedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | program_not_found | No program with that handle in this brand. |
| 422 | document_not_current | That document is not the current text for this program, state and locale. |
| 422 | hosted_consent_required | That scope must be captured on the hosted consent component. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 8192 bytes (8 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/consentsconsents.writeRecord a brand-rendered consent with its render receipt
Records the patient's acceptance against its presentation: acceptedAt between issue and expiry, the receipt's text and label digests equal to the document's, the locale the presentation's, and exactly one checkbox event of the scope's own between issue and acceptance. The record is append-only, with actor provenance and how the patient authenticated to you. Emits clinical.consent.recorded.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
presentationId | string | yes | |
patientAction | "checkbox" | "signature" | yes | |
acceptedAt | string | yes | |
receipt | object | yes | |
signatureSha256 | string | no | For `signature`: SHA-256 of the typed signature (the signature itself is not sent). |
deviceSha256 | string | no | Optional SHA-256 of the patient's user agent and address, as your server saw them. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"scope": "example-scope",
"status": "proposed",
"documentId": "00000000-0000-4000-8000-00000000e001",
"documentVersion": "example-document-version",
"sha256": "example-sha256",
"acceptedAt": "2026-01-01T00:00:00.000Z",
"withdrawnAt": "2026-01-01T00:00:00.000Z",
"withdrawsConsentId": "00000000-0000-4000-8000-00000000e001",
"withdrawn": false,
"channel": "example-channel",
"actorKind": "example-actor-kind",
"locale": "example-locale",
"patientAction": "example-patient-action",
"text": "example-text",
"labelText": "example-label-text"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | presentation_not_found | No presentation with that id for this patient. |
| 409 | presentation_used | That presentation was already used. |
| 422 | presentation_expired | The presentation expired; issue a new one. |
| 422 | rendered_text_mismatch | renderedTextSha256 is not the document's. |
| 422 | label_mismatch | labelSha256 is not the document's label. |
| 422 | checkbox_not_separate | The scope needs exactly one checkbox event of its own. |
| 422 | hosted_consent_required | That scope must be captured on the hosted consent component. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 16384 bytes (16 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/consentspatients.readThe patient's consent records
Every consent record of the patient in your brand, newest first, without evidence internals. Audited.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"scope": "example-scope",
"status": "proposed",
"documentId": "00000000-0000-4000-8000-00000000e001",
"documentVersion": "example-document-version",
"sha256": "example-sha256",
"acceptedAt": "2026-01-01T00:00:00.000Z",
"withdrawnAt": "2026-01-01T00:00:00.000Z",
"withdrawsConsentId": "00000000-0000-4000-8000-00000000e001",
"withdrawn": false,
"channel": "example-channel",
"actorKind": "example-actor-kind",
"locale": "example-locale",
"patientAction": "example-patient-action",
"text": "example-text",
"labelText": "example-label-text"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/consents/{consentId}patients.readOne consent record, with the text exactly as accepted
The record and the exact text and label the patient accepted. Audited individually.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"scope": "example-scope",
"status": "proposed",
"documentId": "00000000-0000-4000-8000-00000000e001",
"documentVersion": "example-document-version",
"sha256": "example-sha256",
"acceptedAt": "2026-01-01T00:00:00.000Z",
"withdrawnAt": "2026-01-01T00:00:00.000Z",
"withdrawsConsentId": "00000000-0000-4000-8000-00000000e001",
"withdrawn": false,
"channel": "example-channel",
"actorKind": "example-actor-kind",
"locale": "example-locale",
"patientAction": "example-patient-action",
"text": "example-text",
"labelText": "example-label-text"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 404 | consent_not_found | No consent with that id for this patient. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/consents/{consentId}/withdrawconsents.writeWithdraw a consent
Records the withdrawal as an append-only successor record. The care team is told on every open case, and the program's consent hold answers consent_required until a new acceptance. Emits clinical.consent.withdrawn.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
reason | string | no |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"scope": "example-scope",
"status": "proposed",
"documentId": "00000000-0000-4000-8000-00000000e001",
"documentVersion": "example-document-version",
"sha256": "example-sha256",
"acceptedAt": "2026-01-01T00:00:00.000Z",
"withdrawnAt": "2026-01-01T00:00:00.000Z",
"withdrawsConsentId": "00000000-0000-4000-8000-00000000e001",
"withdrawn": false,
"channel": "example-channel",
"actorKind": "example-actor-kind",
"locale": "example-locale",
"patientAction": "example-patient-action",
"text": "example-text",
"labelText": "example-label-text"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 404 | consent_not_found | No consent with that id for this patient. |
| 409 | consent_already_withdrawn | This consent is already withdrawn. |
| 422 | consent_not_withdrawable | Only an active legal-kit consent can be withdrawn here. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/intake-drafts/{programHandle}intake.writeThe patient's saved intake draft for a program
The saved answers as an in-progress QuestionnaireResponse, with the revision to send back. Audited.
Example response · 200
{
"data": {
"draftId": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"questionnaireVersionId": "00000000-0000-4000-8000-00000000e001",
"currentQuestionnaireVersionId": "00000000-0000-4000-8000-00000000e001",
"state": "example-state",
"questionnaireResponse": {
"resourceType": "QuestionnaireResponse",
"questionnaire": "example-questionnaire",
"status": "in-progress",
"item": [
null
]
},
"currentStep": "example-current-step",
"revision": "example-revision",
"expiresAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 404 | draft_not_found | No saved intake draft for this program. |
| 404 | program_not_found | No program with that handle in this brand. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/intake-drafts/{programHandle}intake.writeSave the patient's intake draft
Saves the in-progress answers (30-day expiry) against the current questionnaire version, for the program the path names. Send the revision you read as expectedRevision; a stale one answers 409 `conflict` and the saved draft is unchanged. A draft on an older version must be restarted first. On real data the patient needs an accepted referral or enrollment invitation for that program, as submission does, and is refused the same 422 `enrollment_required` without one.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
questionnaireVersionId | string | yes | |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | A two-letter USPS state code or DC. |
questionnaireResponse | object | yes | |
expectedRevision | string | null | no | The revision you read; omit for a new draft. |
currentStep | integer | no |
Example response · 200
{
"data": {
"draftId": "00000000-0000-4000-8000-00000000e001",
"revision": "example-revision",
"savedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 422 | validation_error | The request body or parameters were invalid. |
| 409 | conflict | The draft changed or uses older questions; read it again (or restart). |
| 404 | program_not_found | No program with that handle in this brand. |
| 422 | intake_unavailable | Intake is not available for this brand yet. |
| 422 | enrollment_required | On real data the patient holds no accepted referral or enrollment invitation for this program (and, at submission, the declared state). Enroll the patient in the program first; drafts and submissions answer the same code. |
| 403 | not_permitted | The patient's session cannot hold a draft for this program. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 131072 bytes (128 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/intake-drafts/{programHandle}/restartintake.writeRestart a draft on the current questionnaire
Keeps the previous answers as read-only history and starts the draft again on the current questionnaire version. Idempotent by Idempotency-Key: a retry with the same key and body answers the first restart again (`wasDuplicate` true, the draft as it is now) and restarts nothing twice. The program binds as for saveIntakeDraft (422 `enrollment_required` on real data without an accepted invitation for it).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | A two-letter USPS state code or DC. |
expectedRevision | string | yes |
Example response · 200
{
"data": {
"draftId": "00000000-0000-4000-8000-00000000e001",
"revision": "example-revision",
"questionnaireVersionId": "00000000-0000-4000-8000-00000000e001",
"state": "example-state",
"wasDuplicate": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 404 | draft_not_found | No saved intake draft for this program. |
| 409 | draft_revision_conflict | The saved draft changed; read it again. |
| 409 | draft_already_current | The saved draft already uses the current questions. |
| 409 | conflict | The draft changed while it was restarted, or the questions changed again; read it again. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | program_not_found | No program with that handle in this brand. |
| 422 | intake_unavailable | Intake is not available for this brand yet. |
| 422 | enrollment_required | On real data the patient holds no accepted referral or enrollment invitation for this program (and, at submission, the declared state). Enroll the patient in the program first; drafts and submissions answer the same code. |
| 403 | not_permitted | The patient's session cannot hold a draft for this program. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/intake/evaluateintake.writeEvaluate answers: stop rules and an eligibility preview
Runs the questionnaire's stop rules over the answers so far and previews eligibility (coverage, modality, an open review, the consent hold). Only a stop is persisted (no answers); a stop clears the saved draft. Identity does not block submission: an unverified patient's case waits before the clinician queue.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
programHandle | string | yes | The program's opaque public handle (`prg_` and 24 hex characters). Link the care host's Start page with it: `/start?p=<handle>`. It never changes and carries no program or condition word. |
state | "AL" | "AK" | "AZ" | "AR" | "CA" | "CO" | "CT" | "DE" | "FL" | "GA" | "HI" | "ID" | "IL" | "IN" | "IA" | "KS" | "KY" | "LA" | "ME" | "MD" | "MA" | "MI" | "MN" | "MS" | "MO" | "MT" | "NE" | "NV" | "NH" | "NJ" | "NM" | "NY" | "NC" | "ND" | "OH" | "OK" | "OR" | "PA" | "RI" | "SC" | "SD" | "TN" | "TX" | "UT" | "VT" | "VA" | "WA" | "WV" | "WI" | "WY" | "DC" | yes | A two-letter USPS state code or DC. |
questionnaireResponse | object | yes |
Example response · 200
{
"data": {
"outcome": "continue",
"eligible": false,
"reasonCode": "example-reason-code",
"ruleCode": "example-rule-code",
"patientMessage": "example-patient-message",
"requiredModality": "async",
"flags": [
"example-flags"
],
"consentHold": "example-consent-hold",
"identityRequiredBeforeReview": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 422 | validation_error | The request body or parameters were invalid. |
| 409 | questionnaire_changed | The questionnaire changed; fetch the current version. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 131072 bytes (128 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/intake-submissionsintake.writeSubmit the intake
Submits the completed QuestionnaireResponse through Ambition's intake kernel after checking the consent hold (every required scope accepted on its current text), the questionnaire content hash, the declared location and the intake attestation. The case is created and routed to the physician group. When the patient's identity is not yet verified the case waits before the clinician queue (`caseStatus: awaiting_identity`, outstanding action identity_required) and closes after 14 days. Refusals are 422 with one of consent_required, reconsent_required, hosted_consent_required, location_mismatch, stopped, not_eligible, open_review_exists, modality_unavailable_headless, clinician_unavailable, enrollment_required, answers_incomplete or attestation_required. On real data `enrollment_required` means the patient holds no accepted referral or enrollment invitation for the program and state, the same code a draft answers for its program. Emits clinical.intake.submitted (and clinical.intake.identity_required).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
enrollmentId | string | no | |
programHandle | string | yes | The program's opaque public handle (`prg_` and 24 hex characters). Link the care host's Start page with it: `/start?p=<handle>`. It never changes and carries no program or condition word. |
questionnaireResponse | object | yes | A FHIR R4 QuestionnaireResponse: one answer per linkId, groups allowed. |
contentHash | string | yes | The Questionnaire's `version` you rendered. |
consentIds | array | yes | The legal-kit consents this submission relies on. |
declaredLocation | object | yes | |
attestation | object | yes | The intake attestation from the questionnaire's `intake-attestation` extension, shown verbatim and accepted. |
shippingAddress | object | no |
Example response · 201
{
"data": {
"submissionId": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"questionnaireVersionId": "00000000-0000-4000-8000-00000000e001",
"contentHash": "example-content-hash",
"consentIds": [
"00000000-0000-4000-8000-00000000e001"
],
"declaredLocation": {
"state": "example-state",
"attestedAt": "2026-01-01T00:00:00.000Z"
},
"status": "in_review",
"caseStatus": "awaiting_identity",
"outstandingActions": [
{
"type": "identity_required",
"requestedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
}
],
"submittedAt": "2026-01-01T00:00:00.000Z",
"wasDuplicate": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 422 | validation_error | The request body or parameters were invalid. |
| 409 | questionnaire_changed | The questionnaire or its content hash changed. |
| 422 | consent_required | A required consent has not been accepted on its current text. |
| 422 | stopped | The answers stop this pathway. |
| 422 | not_eligible | The program is not available to this patient in that state. |
| 422 | open_review_exists | An open review for this program already exists. |
| 422 | modality_unavailable_headless | The state requires a video visit (launch week). |
| 422 | location_mismatch | The declared location is stale or not the enrollment's state. |
| 422 | enrollment_required | On real data the patient holds no accepted referral or enrollment invitation for this program (and, at submission, the declared state). Enroll the patient in the program first; drafts and submissions answer the same code. |
| 409 | idempotency_key_reused | That Idempotency-Key was already used and its first submission cannot be read again; use a new key. |
| 403 | billing_suspended | Billing mode B: new intake is paused until the brand's platform invoice is settled (Finances, Platform billing). Not retryable. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 196608 bytes (192 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/intake-submissions/{submissionId}cases.readOne intake submission and its case status
The submission's case, its coarse status and outstanding actions (identity_required while it waits). Never the answers. Needs your BAA with the physician group covering the case. Audited.
Example response · 200
{
"data": {
"submissionId": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"questionnaireVersionId": "00000000-0000-4000-8000-00000000e001",
"contentHash": "example-content-hash",
"consentIds": [
"00000000-0000-4000-8000-00000000e001"
],
"declaredLocation": {
"state": "example-state",
"attestedAt": "2026-01-01T00:00:00.000Z"
},
"status": "in_review",
"caseStatus": "awaiting_identity",
"outstandingActions": [
{
"type": "identity_required",
"requestedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z"
}
],
"submittedAt": "2026-01-01T00:00:00.000Z",
"wasDuplicate": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | submission_not_found | No submission with that id for this patient. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/safety-reportssafety_reports.writeReport a side effect or a product problem
Files the patient's report after you showed the current safety screen (echo its version). A serious answer makes it urgent for the clinical safety team; a report on a compounded or interim-policy product is routed to the prescriber, the dispensing pharmacy and the MedWatch path. Emits clinical.safety_report.received.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
category | "adverse_event" | "product_complaint" | yes | |
description | string | yes | |
onsetAt | string | no | The date the problem started (within five years). |
prescriptionId | string | no | |
orderId | string | no | |
seriousIndicators | array | yes | The seriousness questions the patient answered yes. |
unsureIndicators | array | no | The seriousness questions the patient answered unsure. |
emergencyScreenVersion | string | yes | The `version` of the safety screen you showed. |
productName | string | no | |
lotNumber | string | no |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"status": "example-status",
"seriousness": "example-seriousness",
"caseId": "00000000-0000-4000-8000-00000000e001",
"category": "example-category",
"routedToPhysicianGroup": false,
"wasDuplicate": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 422 | validation_error | The request body or parameters were invalid. |
| 404 | prescription_not_found | No prescription with that id for this patient. |
| 404 | order_not_found | No order with that id for this patient. |
| 422 | emergency_screen_outdated | Show the current safety screen and echo its version. |
| 413 | payload_too_large | The request body exceeds 16384 bytes (16 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/safety-reportscases.readThe patient's safety reports
Reference, category, status and date of each report. Never the assessment or decision. Audited.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"category": "example-category",
"status": "example-status",
"submittedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 503 | temporarily_unavailable | The request could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptions/{prescriptionId}/fillsfills.writeRequest this cycle's fill of a prescription
Call once per billing cycle of your subscription, after you charge the patient. Ambition checks the prescription's clinical currency (approval expiry, refills left, check-ins and monitoring labs due), the declared location, regulatory standing and your fulfillment release, then either creates the refill's pharmacy order (`status: requested`; `clinical.fill.requested`, then `clinical.fill.transmitted` when the pharmacy has it) or holds the fill with one coarse `holdReason` (`clinical.fill.held`). Idempotent per `cycleRef`: the same cycleRef answers the same fill, and repeating it re-evaluates a held fill, so retry a hold with the same cycleRef once it clears. A new cycleRef supersedes an earlier held fill. The first dispense of a new prescription is its own order (sent on signing, or on your release under brand_release); call this for each later cycle. To pause or cancel, do not request a fill; to stop therapy, send a renewal request with `requestedChange: stop` or a message to the care team. When the last authorized refill is used, `clinical.prescription.refills_exhausted` tells you to offer a renewal.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
cycleRef | string | yes | Your own reference for the billing cycle this fill serves (1-80 letters, digits and `._:-`; never PHI). The same cycleRef always answers the same fill: repeat it to retry a held fill once the hold clears. |
declaredLocation | object | yes | The patient's own statement of where they are, captured by your UI for this request. |
shippingAddress | object | no | Where this fill ships, when it differs from the address on the prescription. It must be in the declared state. It is sent to the pharmacy with this fill only. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"cycleRef": "example-cycle-ref",
"status": "held",
"holdReason": "prescription_expired",
"fillNumber": 1,
"orderId": "example_orderId",
"declaredState": "AL",
"requestedAt": "2026-01-01T00:00:00.000Z",
"evaluatedAt": "2026-01-01T00:00:00.000Z",
"refillsRemaining": 1
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 409 | prescription_not_active | The prescription is cancelled, superseded or was never signed. |
| 422 | location_mismatch | The shipping address is not in the declared state. |
| 503 | fill_unavailable | The fill could not be decided. Nothing was changed; retry with the same cycleRef. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptions/{prescriptionId}/fillsfulfillment.readList a prescription's fills
The fills you requested for one of the patient's prescriptions, newest first: status, coarse hold reason, fill number and order id. No address, product or clinical field.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"cycleRef": "example-cycle-ref",
"status": "held",
"holdReason": "prescription_expired",
"fillNumber": 1,
"orderId": "example_orderId",
"declaredState": "AL",
"requestedAt": "2026-01-01T00:00:00.000Z",
"evaluatedAt": "2026-01-01T00:00:00.000Z",
"transmittedAt": "2026-01-01T00:00:00.000Z",
"closedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 503 | fill_unavailable | The fills could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptions/{prescriptionId}/fills/{fillId}fulfillment.readRead one fill
One fill of one of the patient's prescriptions.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"cycleRef": "example-cycle-ref",
"status": "held",
"holdReason": "prescription_expired",
"fillNumber": 1,
"orderId": "example_orderId",
"declaredState": "AL",
"requestedAt": "2026-01-01T00:00:00.000Z",
"evaluatedAt": "2026-01-01T00:00:00.000Z",
"transmittedAt": "2026-01-01T00:00:00.000Z",
"closedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 404 | fill_not_found | No fill with that id on this prescription. |
| 503 | fill_unavailable | The fill could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptions/{prescriptionId}/renewal-eligibilitycases.readWhether a prescription can be renewed now
Whether you may request a renewal now, and if not why; when the approval or the authorized supply runs out and when a renewal opens (the physician group's lead time); refills left; and the program's current check-in questionnaire to render with the renewal request.
Example response · 200
{
"data": {
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"eligible": false,
"reasonCode": "prescription_not_active",
"dueAt": "2026-01-01T00:00:00.000Z",
"eligibleAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"refillsRemaining": 1,
"checkInQuestionnaireVersionId": "00000000-0000-4000-8000-00000000e001"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 503 | renewal_unavailable | Eligibility could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/renewal-requestsrenewals.writeRequest a renewal for the clinician to review
Opens a renewal-kind case for the prescription's physician group (its prescriber while they may still take it) with the patient's check-in answers and the change they ask for. The change is a request only: the clinician decides, writes any new prescription, and you follow the case through the cases read and clinical.case.* events (`clinical.renewal.requested` now). The answers also complete the therapy's check-in. Refused while any review of the program is open. Needs the patient's current verified identity and consent, the state the therapy was approved in, and an asynchronous modality (headless serves async only until video lands). Consent is checked from the evidence on record; there is no consent field.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
prescriptionId | string | yes | The prescription's id (from the prescriptions read). |
questionnaireResponse | object | yes | |
requestedChange | "continue" | "escalate" | "decrease" | "stop" | yes | What the patient asks for. A request only: the clinician decides the dose and whether to continue. |
declaredLocation | object | yes | The patient's own statement of where they are, captured by your UI for this request. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"kind": "renewal",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"requestedChange": "continue",
"checkInId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 409 | prescription_not_active | The prescription is not an active therapy. |
| 409 | prescription_renewed | This prescription has already been renewed. |
| 409 | open_review_exists | A review of this program is already open for the patient. |
| 409 | identity_required | The patient's identity verification is not current. |
| 409 | consent_required | A consent the program requires is missing, out of date, or must be captured hosted. |
| 409 | clinician_unavailable | No clinician of the prescription's physician group can take it now. |
| 422 | location_mismatch | The declared location is not the state the therapy was approved in. |
| 422 | questionnaire_not_found | Not this program's published check-in questionnaire. |
| 422 | answers_invalid | An answer names no item of the questionnaire, or is not a value. |
| 422 | answers_incomplete | A required item is unanswered. |
| 422 | modality_unavailable_headless | This follow-up needs a synchronous visit, which headless care does not serve yet. |
| 503 | follow_up_unavailable | The request could not be recorded. Nothing was changed; retry. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 49152 bytes (48 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/check-inscases.readList the patient's check-ins
Due, overdue and submitted check-ins of the patient's therapies under the physician group's monitoring cadence, and the next one each active therapy needs (`id` null until it is scheduled). An overdue check-in holds fills (`checkin_due`). `clinical.checkin.due` fires when one enters its notice period. Only therapies whose case your BAA covers are listed.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
prescriptionId | string | no | Only this therapy's check-ins. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"status": "upcoming",
"dueAt": "2026-01-01T00:00:00.000Z",
"submittedAt": "2026-01-01T00:00:00.000Z",
"reviewCaseId": "00000000-0000-4000-8000-00000000e001"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 422 | validation_error | Query parameters were invalid. |
| 503 | check_in_unavailable | Check-ins could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/check-insrenewals.writeSubmit a check-in
Records the patient's answers to the program's check-in questionnaire for one active therapy and opens a check_in-kind case for the clinician. Answers the program's protocol rules flag hold fills (`checkin_under_review`) until the clinician decides; otherwise fills continue. Refused while any review of the program is open (a renewal request also counts as the check-in).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
prescriptionId | string | yes | The prescription's id (from the prescriptions read). |
questionnaireResponse | object | yes | |
declaredLocation | object | yes | The patient's own statement of where they are, captured by your UI for this request. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"status": "submitted",
"dueAt": "2026-01-01T00:00:00.000Z",
"submittedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 404 | prescription_not_found | No prescription with that id belongs to this patient. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this prescription's case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 409 | prescription_not_active | The prescription is not an active therapy. |
| 409 | prescription_renewed | This prescription has already been renewed. |
| 409 | open_review_exists | A review of this program is already open for the patient. |
| 409 | identity_required | The patient's identity verification is not current. |
| 409 | consent_required | A consent the program requires is missing, out of date, or must be captured hosted. |
| 409 | clinician_unavailable | No clinician of the prescription's physician group can take it now. |
| 422 | location_mismatch | The declared location is not the state the therapy was approved in. |
| 422 | questionnaire_not_found | Not this program's published check-in questionnaire. |
| 422 | answers_invalid | An answer names no item of the questionnaire, or is not a value. |
| 422 | answers_incomplete | A required item is unanswered. |
| 422 | modality_unavailable_headless | This follow-up needs a synchronous visit, which headless care does not serve yet. |
| 503 | follow_up_unavailable | The request could not be recorded. Nothing was changed; retry. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 49152 bytes (48 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/casescases.readList the patient's cases
The patient's clinical reviews, newest first: coarse stage and who acts next, the assigned clinician's required disclosure (name, credential, licence state; bio only when they opted in), outstanding actions with the clinician's patient-facing prompts, the decision and its patient-facing note, and the prescription and order ids. Only cases whose physician group your BAA covers are listed. Follow changes with clinical.case.* events.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"kind": "initial",
"stage": "awaiting_identity",
"patientStatus": "with_care_team",
"programKey": "example-program-key",
"state": "example-state",
"requiredModality": "async",
"clinician": {
"displayName": "example-display-name",
"credential": "example-credential",
"licenseState": "example-license-state",
"bio": "example-bio",
"photoUrl": "example-photo-url"
},
"outstandingActions": [
{
"type": "more_info",
"prompt": "example-prompt",
"requestedAt": "2026-01-01T00:00:00.000Z"
}
],
"decision": {
"outcome": "approved",
"patientFacingNote": "example-patient-facing-note",
"decidedAt": "2026-01-01T00:00:00.000Z",
"refundExpected": false
},
"prescriptionIds": [
"00000000-0000-4000-8000-00000000e001"
],
"orderIds": [
"example-order-ids"
],
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 503 | case_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/cases/{caseId}cases.readRead one case
One of the patient's cases, as in the list. The BAA must cover the case's physician group.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"kind": "initial",
"stage": "awaiting_identity",
"patientStatus": "with_care_team",
"programKey": "example-program-key",
"state": "example-state",
"requiredModality": "async",
"clinician": {
"displayName": "example-display-name",
"credential": "example-credential",
"licenseState": "example-license-state",
"bio": "example-bio",
"photoUrl": "example-photo-url"
},
"outstandingActions": [
{
"type": "more_info",
"prompt": "example-prompt",
"requestedAt": "2026-01-01T00:00:00.000Z"
}
],
"decision": {
"outcome": "approved",
"patientFacingNote": "example-patient-facing-note",
"decidedAt": "2026-01-01T00:00:00.000Z",
"refundExpected": false
},
"prescriptionIds": [
"00000000-0000-4000-8000-00000000e001"
],
"orderIds": [
"example-order-ids"
],
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 503 | case_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/cases/{caseId}/more-info-responsescases.writeAnswer the clinician's request for more information
Posts the patient's answer on the case's care-team thread and returns the case to the clinician's review (`clinical.case.info_received`). Only while the case's stage is `needs_info`. Send the patient's own words; content your AI wrote must carry Ambition-Content-Source: automation and is badged for the clinician.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
message | string | no | A free-text answer. |
answers | array | no | Structured answers, one per question. |
Example response · 201
{
"data": {
"caseId": "00000000-0000-4000-8000-00000000e001",
"threadId": "00000000-0000-4000-8000-00000000e001",
"messageId": "00000000-0000-4000-8000-00000000e001",
"stage": "awaiting_identity"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 409 | case_not_awaiting_info | The clinician is not waiting for information on this case. |
| 409 | clinician_not_assigned | No clinician is assigned to the case. |
| 503 | case_unavailable | Could not be completed. Nothing was changed; retry. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 32768 bytes (32 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/cases/{caseId}/cancelcases.writeCancel a case before the clinician decides
Withdraws the patient's request before any decision (`clinical.case.cancelled`). Refused once a clinician has decided, and while a payment Ambition collected is still in progress. As merchant of record you refund under your own terms. Idempotent: cancelling a cancelled case answers it again.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
reason | "patient_request" | "duplicate" | "no_longer_needed" | "cost" | "other" | yes | Why the patient withdraws the request. A code only; never PHI. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"kind": "initial",
"stage": "awaiting_identity",
"patientStatus": "with_care_team",
"programKey": "example-program-key",
"state": "example-state",
"requiredModality": "async",
"clinician": {
"displayName": "example-display-name",
"credential": "example-credential",
"licenseState": "example-license-state",
"bio": "example-bio",
"photoUrl": "example-photo-url"
},
"outstandingActions": [
{
"type": "more_info",
"prompt": "example-prompt",
"requestedAt": "2026-01-01T00:00:00.000Z"
}
],
"decision": {
"outcome": "approved",
"patientFacingNote": "example-patient-facing-note",
"decidedAt": "2026-01-01T00:00:00.000Z",
"refundExpected": false
},
"prescriptionIds": [
"00000000-0000-4000-8000-00000000e001"
],
"orderIds": [
"example-order-ids"
],
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 409 | case_already_decided | The clinician has decided; the case cannot be cancelled. |
| 409 | case_payment_open | A payment Ambition collected is in progress for this case. |
| 503 | case_unavailable | Could not be completed. Nothing was changed; retry. |
| 413 | payload_too_large | The request body exceeds 1024 bytes (1 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptionsprescriptions.readList the patient's prescriptions
Written prescriptions, newest first, minimum necessary: product, strength, form, route, directions, quantity, refills authorized and remaining, status and one coarse hold. Never the prescriber's DEA or NPI, the pharmacy, transport attempts or clarification text.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"product": "example-product",
"strength": "example-strength",
"form": "example-form",
"route": "example-route",
"patientDirections": "example-patient-directions",
"quantity": 1,
"quantityUnit": "example-quantity-unit",
"daysSupply": 1,
"refillsAuthorized": 1,
"refillsRemaining": 1,
"status": "active",
"hold": "none",
"writtenAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"renewalEligibleAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 503 | prescription_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptions/{prescriptionId}prescriptions.readRead one prescription
One of the patient's written prescriptions, as in the list.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"product": "example-product",
"strength": "example-strength",
"form": "example-form",
"route": "example-route",
"patientDirections": "example-patient-directions",
"quantity": 1,
"quantityUnit": "example-quantity-unit",
"daysSupply": 1,
"refillsAuthorized": 1,
"refillsRemaining": 1,
"status": "active",
"hold": "none",
"writtenAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"renewalEligibleAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | prescription_not_found | No written prescription with that id belongs to this patient. |
| 503 | prescription_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/ordersfulfillment.readList the patient's pharmacy orders
Pharmacy orders of the patient's prescriptions, newest first: coarse status, exception code, tracking number, shipped and delivered times, items and, when the clinician cancelled the prescription while the pharmacy might hold the order, where that cancellation stands (`cancellation`). No pharmacy ids, partner references, costs, callback bodies or the pharmacy's reason for refusing a cancellation.
Example response · 200
{
"data": [
{
"id": "example_id",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"status": "created",
"exception": "example-exception",
"carrier": "example-carrier",
"trackingNumber": "example-tracking-number",
"trackingUrl": "example-tracking-url",
"shippedAt": "2026-01-01T00:00:00.000Z",
"deliveredAt": "2026-01-01T00:00:00.000Z",
"items": [
{
"product": "example-product",
"quantity": 1,
"quantityUnit": "example-quantity-unit"
}
],
"cancellation": {
"state": "requested",
"requestedAt": "2026-01-01T00:00:00.000Z",
"answeredAt": "2026-01-01T00:00:00.000Z"
},
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 503 | order_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/orders/{orderId}fulfillment.readRead one pharmacy order
One of the patient's pharmacy orders, as in the list. After `clinical.prescription.cancelled` or `clinical.order.exception`, read `cancellation` here to learn whether the pharmacy still holds the order.
Example response · 200
{
"data": {
"id": "example_id",
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"status": "created",
"exception": "example-exception",
"carrier": "example-carrier",
"trackingNumber": "example-tracking-number",
"trackingUrl": "example-tracking-url",
"shippedAt": "2026-01-01T00:00:00.000Z",
"deliveredAt": "2026-01-01T00:00:00.000Z",
"items": [
{
"product": "example-product",
"quantity": 1,
"quantityUnit": "example-quantity-unit"
}
],
"cancellation": {
"state": "requested",
"requestedAt": "2026-01-01T00:00:00.000Z",
"answeredAt": "2026-01-01T00:00:00.000Z"
},
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | order_not_found | No order with that id belongs to this patient. |
| 503 | order_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/threadsmessages.readList the patient's message threads
The care-team thread of each case your BAA covers, and the patient's support and billing conversations with Ambition care operations, most recent first, with unread care-team messages.
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"topic": "clinical",
"caseId": "00000000-0000-4000-8000-00000000e001",
"subject": "example-subject",
"status": "open",
"lastMessageAt": "2026-01-01T00:00:00.000Z",
"unreadCount": 1,
"createdAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 503 | message_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/threadsmessages.writeStart a conversation
`clinical`: posts the patient's message to the clinician assigned to `caseId` (the case's care-team thread). `support`, `billing`: opens a request with Ambition care operations. `clinical.message.created` carries ids and the sender role only, never the body.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
topic | "clinical" | "support" | "billing" | yes | |
caseId | string | no | Required for `clinical`: the case whose clinician receives the message. |
body | string | yes | The patient's first message (10 to 1500 characters for support and billing). |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"topic": "clinical",
"caseId": "00000000-0000-4000-8000-00000000e001",
"messageId": "00000000-0000-4000-8000-00000000e001"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 409 | clinician_not_assigned | No clinician is assigned to the case yet. |
| 422 | case_required | A clinical thread names its case. |
| 503 | message_unavailable | Could not be completed. Nothing was changed; retry. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 24576 bytes (24 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/threads/{threadId}/messagesmessages.readRead a thread's messages
Messages of one thread, newest first, 50 per page (`limit` up to 100), with `meta.nextCursor` for older ones. Internal care-team notes are never included.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
cursor | string | no | `meta.nextCursor` of the previous page. |
limit | integer | no | Default 50. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"threadId": "00000000-0000-4000-8000-00000000e001",
"senderRole": "patient",
"contentSource": "patient",
"body": "example-body",
"createdAt": "2026-01-01T00:00:00.000Z",
"readAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | thread_not_found | No thread with that id belongs to this patient. |
| 422 | validation_error | Query parameters were invalid. |
| 503 | message_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/threads/{threadId}/messagesmessages.writeSend the patient's message
Posts the patient's message on an open thread. Attachments arrive with uploads (launch-week). Only the patient's side is ever written through the API; care-team replies are written by licensed clinicians.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
body | string | yes | The patient's message (at most 1500 characters on support and billing threads). |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"threadId": "00000000-0000-4000-8000-00000000e001",
"createdAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 404 | thread_not_found | No thread with that id belongs to this patient. |
| 409 | thread_closed | The thread is closed; start a new one. |
| 409 | clinician_not_assigned | No clinician is assigned to the case. |
| 503 | message_unavailable | Could not be completed. Nothing was changed; retry. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 24576 bytes (24 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/messages/{messageId}/readmessages.writeRecord that the patient read a care-team message
Call when your UI shows the patient a care-team message, so the clinician sees it was read. Support messages carry no read receipt (409 read_receipt_unsupported).
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"readAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 404 | message_not_found | No message with that id belongs to this patient. |
| 409 | read_receipt_unsupported | Only a care-team message delivered to the patient is marked read. |
| 503 | message_unavailable | Could not be completed. Nothing was changed; retry. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/unread-summarymessages.readCount unread care-team messages
Care-team messages delivered to the patient and not yet marked read, in total and per thread.
Example response · 200
{
"data": {
"unreadCount": 1,
"threads": [
{
"threadId": "00000000-0000-4000-8000-00000000e001",
"unreadCount": 1,
"latestAt": "2026-01-01T00:00:00.000Z"
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 503 | message_unavailable | Could not be completed. Nothing was changed; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/lab-requirementslabs.readWhat labs the patient's program needs, and the labs gate
The physician group's lab protocol for the program (baseline labs before prescribing, monitoring labs on an interval once therapy starts; for example a 40+ biomarker peptide baseline and a CBC and CMP every 6 months) and where the patient stands on each: met, under review, required or due, with due dates and the result that satisfies it. `gate` is what holds prescribing and fills (null when labs hold nothing). Give `caseId`, or `programId` before the patient has a case (requirements then follow the patient's latest case in the program). Use it to tell the patient which draw to book; `clinical.case.*` and `clinical.monitoring.labs_due` tell you when to look.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
caseId | string | no | The case the labs are for (from the cases read). |
programHandle | string | no | The program's public handle (`prg_...`, from the catalog, eligibility or enrollments), when there is no case yet (requirements follow the patient's latest case in it). |
programId | string | no | Deprecated: the `programId` an earlier answer of this endpoint returned. Use programHandle. |
Example response · 200
{
"data": {
"caseId": "00000000-0000-4000-8000-00000000e001",
"programId": "00000000-0000-4000-8000-00000000e001",
"programHandle": "example-program-handle",
"gate": "labs_required",
"requirementSetId": "00000000-0000-4000-8000-00000000e001",
"requirements": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"key": "example-key",
"label": "example-label",
"kind": "baseline",
"loincCodes": [
"example-loinc-codes"
],
"minCodesPresent": 1,
"maxAgeDays": 1,
"intervalDays": 1,
"acceptedSources": [
"brand_arranged"
],
"status": "met",
"dueAt": "2026-01-01T00:00:00.000Z",
"satisfiedByResultId": "00000000-0000-4000-8000-00000000e001",
"lastCollectedAt": "2026-01-01T00:00:00.000Z",
"pendingResultId": "00000000-0000-4000-8000-00000000e001"
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 404 | program_not_found | No program with that id in your brand. |
| 422 | validation_error | Give caseId or programId (valid ids). |
| 422 | program_mismatch | The case is not in that program. |
| 503 | labs_unavailable | Lab requirements could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/lab-resultslabs.writeSubmit a lab result
Posts one performed-lab report for a case: structured LOINC observations (value, unit, reference range and the lab's interpretation code), the source (`brand_arranged` or `patient_supplied`), the performing lab's CLIA number and collectedAt. Ambition flags it on receipt (`flag`), assigns it to the case's clinician for review and sign-off and sends `clinical.lab.result_received`; a critical value also alerts Care operations and sends `clinical.lab.critical`. Until the clinician signs off a critical result, prescribing and fills hold (`labs_critical`); `clinical.lab.reviewed` follows the sign-off. Idempotent by Idempotency-Key and, when you send it, by `externalRef`. Values are stored as reported; Ambition does not convert units.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
caseId | string | yes | The case the labs are for (from the cases read). |
source | "brand_arranged" | "patient_supplied" | yes | `brand_arranged`: you ordered the draw (through your lab partner or aggregator). `patient_supplied`: the patient brought a report. A result of a requisition Ambition's clinician signed is reported by that lab, not by you: it reads `ambition_ordered` in the results list. |
performingLab | object | yes | |
collectedAt | string | yes | When the specimen was collected (not in the future; at most 5 years ago). |
reportedAt | string | no | When the lab reported it. |
panelCode | string | no | The panel's LOINC code, e.g. `24323-8` (CMP). |
panelName | string | no | |
externalRef | string | no | Your own reference for this report (never PHI). The same externalRef answers the same result. |
observations | array | yes | One entry per analyte, each LOINC code at most once. |
Example response · 201
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"source": "brand_arranged",
"flag": "normal",
"observationCount": 1,
"abnormalCount": 1,
"criticalCount": 1,
"reviewStatus": "pending_review",
"collectedAt": "2026-01-01T00:00:00.000Z",
"receivedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"replayed": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 400 | idempotency_key_required | Provide a 16 to 255 character Idempotency-Key. |
| 400 | invalid_json | The body is not valid JSON. |
| 422 | validation_error | The body does not match the contract. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`. |
| 409 | idempotency_conflict | This Idempotency-Key was used with a different body. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 409 | external_ref_conflict | That externalRef already names another patient's or case's report. |
| 422 | loinc_invalid | A code is not a LOINC code with a valid check digit. |
| 422 | clia_invalid | The CLIA number is not two digits, D and seven digits. |
| 422 | collected_at_invalid | collectedAt is in the future or more than 5 years ago, or reportedAt precedes it. |
| 422 | observations_invalid | An observation's value, unit or reference range is invalid, or a code repeats. |
| 422 | lab_order_not_found | An `ambition_ordered` result names an Ambition lab order and is reported by the lab, not submitted here. |
| 503 | labs_unavailable | The result could not be recorded. Nothing was changed; retry with the same Idempotency-Key. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 65536 bytes (64 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/lab-resultslabs.readList the patient's lab results
The patient's lab results, newest first: source, performing lab, collectedAt, flag and counts, review status and disposition. Read one result for its values. Only cases whose physician group your BAA covers are listed.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
caseId | string | no | Only this case's results. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"source": "brand_arranged",
"flag": "normal",
"observationCount": 1,
"abnormalCount": 1,
"criticalCount": 1,
"reviewStatus": "pending_review",
"collectedAt": "2026-01-01T00:00:00.000Z",
"receivedAt": "2026-01-01T00:00:00.000Z",
"performingLab": {
"name": "example-name",
"cliaNumber": "example-clia-number"
},
"panelCode": "example-panel-code",
"panelName": "example-panel-name",
"reviewedAt": "2026-01-01T00:00:00.000Z",
"disposition": "no_action"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 422 | validation_error | Query parameters were invalid. |
| 503 | labs_unavailable | Lab results could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/lab-results/{labResultId}labs.readRead one lab result
One result with its observations (as submitted, with Ambition's flag for each) and the clinician's sign-off as you may show it: status, time, disposition and the clinician's note to the patient. The care-team note is never served.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"caseId": "00000000-0000-4000-8000-00000000e001",
"source": "brand_arranged",
"performingLab": {
"name": "example-name",
"cliaNumber": "example-clia-number"
},
"panelCode": "example-panel-code",
"panelName": "example-panel-name",
"collectedAt": "2026-01-01T00:00:00.000Z",
"reportedAt": "2026-01-01T00:00:00.000Z",
"receivedAt": "2026-01-01T00:00:00.000Z",
"externalRef": "example-external-ref",
"flag": "normal",
"observationCount": 1,
"abnormalCount": 1,
"criticalCount": 1,
"review": {
"status": "pending_review",
"reviewedAt": "2026-01-01T00:00:00.000Z",
"disposition": "no_action",
"patientNote": "example-patient-note"
},
"observations": [
{
"code": "example-code",
"display": "example-display",
"value": 1,
"unit": "example-unit",
"referenceRange": {
"low": 1,
"high": 1,
"text": "example-text"
},
"interpretation": "N",
"flag": "normal"
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | lab_result_not_found | No lab result with that id belongs to this patient. |
| 503 | labs_unavailable | The result could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/lab-orderslabs.readList the patient's lab orders
The lab requisitions a clinician signed for the patient (a panel of the program's lab protocol, or one the clinician ordered), newest first: the lab, how the specimen is collected, fasting hours, the lab's instructions, the status, the times it moved and how long the requisition is valid. Use it to tell the patient how to complete the draw after `clinical.lab.order_signed`, and to follow `clinical.lab.order_updated`. A requisition not yet signed is not listed. No diagnosis code, clinician signature or lab account is served. Only cases whose physician group your BAA covers are listed.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
caseId | string | no | Only this case's lab orders. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"caseId": "00000000-0000-4000-8000-00000000e001",
"status": "signed",
"panelKey": "example-panel-key",
"panelName": "example-panel-name",
"lab": {
"name": "example-name",
"sandbox": false
},
"collectionMode": "walk_in",
"fastingHours": 1,
"instructions": [
"example-instructions"
],
"signedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"sentAt": "2026-01-01T00:00:00.000Z",
"acceptedAt": "2026-01-01T00:00:00.000Z",
"collectedAt": "2026-01-01T00:00:00.000Z",
"receivedAt": "2026-01-01T00:00:00.000Z",
"resultedAt": "2026-01-01T00:00:00.000Z",
"closedAt": "2026-01-01T00:00:00.000Z",
"closedReason": "example-closed-reason",
"resultIds": [
"00000000-0000-4000-8000-00000000e001"
]
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | case_not_found | No case with that id belongs to this patient. |
| 422 | validation_error | Query parameters were invalid. |
| 503 | labs_unavailable | Lab orders could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/lab-orders/{labOrderId}labs.readRead one lab order
One signed lab requisition of the patient, as in the list: what to tell the patient (the lab, collection, fasting, instructions, validity), where it stands and the results reported for it.
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"reference": "example-reference",
"caseId": "00000000-0000-4000-8000-00000000e001",
"status": "signed",
"panelKey": "example-panel-key",
"panelName": "example-panel-name",
"lab": {
"name": "example-name",
"sandbox": false
},
"collectionMode": "walk_in",
"fastingHours": 1,
"instructions": [
"example-instructions"
],
"signedAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:00:00.000Z",
"sentAt": "2026-01-01T00:00:00.000Z",
"acceptedAt": "2026-01-01T00:00:00.000Z",
"collectedAt": "2026-01-01T00:00:00.000Z",
"receivedAt": "2026-01-01T00:00:00.000Z",
"resultedAt": "2026-01-01T00:00:00.000Z",
"closedAt": "2026-01-01T00:00:00.000Z",
"closedReason": "example-closed-reason",
"resultIds": [
"00000000-0000-4000-8000-00000000e001"
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this case. |
| 404 | lab_order_not_found | No signed lab order with that id belongs to this patient. |
| 503 | labs_unavailable | The lab order could not be read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/notificationsnotifications.ackThe patient's notices: what you still owe them
Notices raised for the patient, newest first. `state=open` (default) lists the ones routed to you and not yet acknowledged: reconcile against it after downtime, so no clinically required notice misses its deadline. A notice about a case whose physician group your BAA no longer covers is left out. Recorded in the access log.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
state | "open" | "all" | no | `open`: yours to send and not yet acknowledged. `all`: every notice. |
limit | integer | no | Notices per page, newest first, 1 to 100. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"templateKey": "urgent_clinician_message",
"clinicallyRequired": false,
"urgency": "urgent",
"route": "brand_webhook",
"caseId": "00000000-0000-4000-8000-00000000e001",
"requestedAt": "2026-01-01T00:00:00.000Z",
"ackDeadline": "example-ack-deadline",
"acknowledgedAt": "2026-01-01T00:00:00.000Z",
"channel": "email",
"ambitionNotice": "not_needed"
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 422 | validation_error | Query parameters were invalid. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/notifications/{notificationId}/deliverednotifications.ackAcknowledge that you delivered a notice
Records that you told the patient (channel and time). The first acknowledgment stands; repeating it returns the notice as acknowledged. A clinically required notice acknowledged by its deadline cancels Ambition's content-free fallback. Stamped with your key, the patient's session reference and whether your automation sent it.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
channel | "email" | "sms" | "push" | "in_app" | yes | How you delivered the notice. |
deliveredAt | string | yes | When you delivered it (ISO 8601). |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"templateKey": "urgent_clinician_message",
"clinicallyRequired": false,
"urgency": "urgent",
"route": "brand_webhook",
"caseId": "00000000-0000-4000-8000-00000000e001",
"requestedAt": "2026-01-01T00:00:00.000Z",
"ackDeadline": "example-ack-deadline",
"acknowledgedAt": "2026-01-01T00:00:00.000Z",
"channel": "email",
"ambitionNotice": "not_needed"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send `Ambition-Patient-Session` and `Ambition-Patient-Auth`. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | patient_access_unavailable | Patient access is temporarily unavailable. Nothing was processed. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used for another notice or body. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group responsible for this notice's case. |
| 404 | notification_not_found | No notice with that id for this patient. |
| 422 | validation_error | The body was invalid, or `deliveredAt` is in the future or before the notice was raised. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/patients/{patientId}/prescriptions/{prescriptionId}/release-fulfillmentfulfillment.writeRelease a signed prescription to the pharmacy after you captured payment
For a program whose fulfillment release is `brand_release` (you are merchant of record and charge once the prescription is written): Ambition does not transmit a signed prescription until you call this. Call it after you capture the patient's payment. A prescription you do not release within 7 days of signature is never transmitted; the clinician and the patient are told (`clinical.prescription.held`), and refunding is yours. A release never overrides a clinical or regulatory hold: the answer says when one still stands. Idempotent: releasing again answers the current state with `replayed: true`.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
paymentReference | string | no | Your own reference for the payment you captured (for example your processor's charge id). Ambition keeps only its SHA-256 digest, to reconcile; it is never shown or sent anywhere. |
Example response · 200
{
"data": {
"prescriptionId": "00000000-0000-4000-8000-00000000e001",
"release": "released",
"releasedAt": "2026-01-01T00:00:00.000Z",
"transmission": "awaiting_order",
"holdReason": "regulatory",
"replayed": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 403 | entity_baa_missing | Your BAA does not cover the physician group of this prescription's case. |
| 404 | prescription_not_found | No prescription with that id for this patient. |
| 409 | release_not_required | The program transmits on signature (`transmission_release` is `auto`). |
| 409 | prescription_not_releasable | The prescription is not signed, or was cancelled, rejected or replaced. |
| 409 | release_expired | More than 7 days passed since signature; it will not be transmitted. Ask the clinician for a new prescription. |
| 503 | release_unavailable | The release could not be recorded. Nothing changed; retry with the same Idempotency-Key. |
| 403 | patient_admission_closed | This deployment is not admitting patients or taking patient data yet (a real-data deployment before its real-PHI launch, or without a loaded launch authorization). Nothing was processed and the Idempotency-Key is not spent. Never answered on the sandbox. |
| 413 | payload_too_large | The request body exceeds 4096 bytes (4 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/billing/ledgerbilling.readYour billing ledger: fees, prefund or credit position, standing, and ledger lines
Billing mode B (you are merchant of record): what you owe Ambition at your fixed fees. One professional and one platform line per decided review (approved or declined alike) and one pharmacy and one platform line per fill, newest first with cursor pagination, plus your fee schedule, prefund or credit position and standing (a nonpayment suspension pauses new intake; patients in care keep a 30-day continuity grace). PHI-free by construction: a line is named by its own opaque id and never by a case, order, patient, program or product, so this is a standard key scope (`billing.read`, minted with `finance.read`), not a patient scope, and it is never offered to a marketplace app. Brand and plane come from the key: a sandbox key reads the sandbox plane's test ledger, a live key the live one, and another brand's rows are not addressable. Never cached.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
basis | "review" | "fill" | "adjustment" | no | Only lines of this basis. |
invoiced | "true" | "false" | no | `true`: only lines already on an invoice; `false`: only uninvoiced lines. |
cursor | string | no | The `nextCursor` of the previous page. |
limit | integer | no | Per page, 1 to 100. |
Example response · 200
{
"data": {
"environment": "example-environment",
"livemode": false,
"schedule": {
"version": 1,
"currency": "usd",
"reviewProfessionalCents": 1,
"reviewPlatformCents": 1,
"fillPharmacyCents": 1,
"fillPlatformCents": 1,
"settlement": "prefund",
"creditLimitCents": 1,
"prefundFloorCents": 1,
"paymentTermsDays": 1,
"effectiveFrom": "example-effective-from"
},
"position": {
"currency": "usd",
"prefundedCents": 1,
"accruedCents": 1,
"balanceCents": 1,
"uninvoicedCents": 1,
"openInvoicedCents": 1,
"pastDueCents": 1
},
"standing": {
"state": "good",
"reason": "invoice_past_due",
"suspendedAt": "2026-01-01T00:00:00.000Z",
"continuityUntil": "example-continuity-until"
},
"lines": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"occurredAt": "2026-01-01T00:00:00.000Z",
"basis": "review",
"kind": "professional",
"amountCents": 1,
"currency": "usd",
"invoiceNumber": "example-invoice-number",
"livemode": false
}
]
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"hasMore": false,
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Query parameters, or the cursor, were invalid. |
| 503 | billing_unavailable | The ledger could not be read. Nothing was changed; retry. |
/api/v1/billing/invoicesbilling.readYour invoices from Ambition
The B2B invoices Ambition issued you under billing mode B, newest first with cursor pagination: period, counts, totals by kind (always summing to the total), status (`past_due` once an open invoice passes its due date), due and paid dates. A prefund brand's invoice is a statement settled from its balance. PHI-free by construction: a line is named by its own opaque id and never by a case, order, patient, program or product, so this is a standard key scope (`billing.read`, minted with `finance.read`), not a patient scope, and it is never offered to a marketplace app. Brand and plane come from the key: a sandbox key reads the sandbox plane's test ledger, a live key the live one, and another brand's rows are not addressable. Never cached.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
status | "open" | "past_due" | "paid" | "void" | "uncollectible" | no | Only invoices in this state (`past_due`: open past its due date). |
cursor | string | no | The `nextCursor` of the previous page. |
limit | integer | no | Per page, 1 to 100. |
Example response · 200
{
"data": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"number": "example-number",
"periodStart": "example-period-start",
"periodEnd": "example-period-end",
"settlement": "prefund",
"reviewCount": 1,
"fillCount": 1,
"professionalCents": 1,
"pharmacyCents": 1,
"platformCents": 1,
"adjustmentCents": 1,
"totalCents": 1,
"currency": "usd",
"status": "open",
"issuedAt": "2026-01-01T00:00:00.000Z",
"dueAt": "2026-01-01T00:00:00.000Z",
"paidAt": "2026-01-01T00:00:00.000Z",
"livemode": false,
"stripeLinked": false
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment",
"hasMore": false,
"nextCursor": "example-next-cursor"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 422 | validation_error | Query parameters, or the cursor, were invalid. |
| 503 | billing_unavailable | The ledger could not be read. Nothing was changed; retry. |
/api/v1/sandbox/patients/{patientId}/cases/{caseId}/simulatecases.writeSandbox: the test clinician decides a case (approve prescribes)
The sandbox test clinician takes the case's review task and records `approve`, `needs_info` or `decline` with the real decision command, so `clinical.case.approved`, `.needs_info` or `.declined` (with `refundExpected` in mode B) reaches your receiver. On approve it also writes, signs and routes the prescription through the prescription rail (`clinical.prescription.signed`, `clinical.order.created`); transmission then waits on every real hold (release, contact, labs). Real gates hold: a case waiting on labs answers 409 `labs_required`; a case assigned to another clinician 409 `sandbox_clinician_unavailable`. Sandbox only: a sandbox phi_server key, a synthetic brand and the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). The actor is synthetic and labelled; the commands, gates and events are the real ones, with `livemode: false`. 60 loops (patients) and 600 simulations per brand per hour. Idempotent: the same Idempotency-Key answers the first simulation.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
action | "approve" | "needs_info" | "decline" | yes | The sandbox test clinician's decision: `approve`, `needs_info` or `decline`. |
reason | string | no | The patient-facing reason (needs_info and decline); a synthetic default is used when omitted. Never real clinical content. |
prescribe | boolean | no | On approve (default true): also write, sign and route the prescription for the case's first prescribable product. |
pharmacyProductId | string | no | On approve with prescribe: which product of the case's prescription workspace to prescribe. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"object": "sandbox_simulation",
"kind": "case",
"action": "example-action",
"objectId": "example_objectId",
"patientId": "00000000-0000-4000-8000-00000000e001",
"actor": {
"kind": "sandbox_test_clinician",
"label": "example-label"
},
"result": {},
"events": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"type": "example-type",
"object": "example-object",
"objectId": "example_objectId",
"deliveryState": "example-delivery-state",
"recordedAt": "2026-01-01T00:00:00.000Z"
}
],
"standardEvents": [
"example-standard-events"
],
"livemode": false,
"simulatedAt": "2026-01-01T00:00:00.000Z",
"replayed": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 422 | validation_failed | The simulation request is invalid. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 422 | simulation_action_invalid | That action is not one this object takes. |
| 409 | idempotency_key_reused | This Idempotency-Key was used for a different simulation. |
| 429 | sandbox_simulation_rate_limited | At most 600 sandbox simulations per brand per hour. |
| 429 | sandbox_loop_rate_limited | At most 60 sandbox loops (patients) per brand per hour. |
| 503 | simulation_unavailable | The simulation could not run. Nothing changed; retry with the same Idempotency-Key. |
| 404 | case_not_found | No case with that id for this patient. |
| 409 | case_not_assigned | The case has no clinician yet; the sandbox network assigns one when the case enters review. |
| 409 | sandbox_clinician_unavailable | The case is with a clinician who is not the sandbox test clinician. |
| 409 | sandbox_clinician_not_credentialed | The sandbox test clinician is not licensed in the case's state. |
| 409 | labs_required | The program's lab requirements must be met and signed off before this case is approved. |
| 409 | case_not_reviewable | The case is not in review. |
| 409 | decision_refused | The sandbox test clinician could not record the decision; a real gate refused it. |
| 409 | prescription_refused | The sandbox test clinician could not write the prescription; a real gate refused it. |
| 409 | no_prescribable_product | No product of the sandbox formulary fits this case. |
| 413 | payload_too_large | The request body exceeds 2048 bytes (2 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/patients/{patientId}/prescriptions/{prescriptionId}/simulatefulfillment.writeSandbox: the test pharmacy ships or delivers; policy withdrawn; renewal due
`acknowledge`, `accept`, `ship`, `deliver` or `exception`: the sandbox test pharmacy reports the order's status as an authenticated partner callback, earlier steps first (`clinical.order.acknowledged`, `.accepted`, `.in_production`, `.shipped`, `.delivered`, `.exception`). The order must have been transmitted; name a refill's order with `orderId` and it walks its own steps through to delivered. `policy_withdrawn`: for a prescription of a regulated substance, the real regulatory hold on this prescription (`clinical.prescription.held`, hold `policy_withdrawn`) and `formulary.policy_changed` to you only. `renewal_due`: the therapy's check-in falls due now (`clinical.checkin.due`) and the `renewal_due` notice is requested (`clinical.notification.requested`). Sandbox only: a sandbox phi_server key, a synthetic brand and the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). The actor is synthetic and labelled; the commands, gates and events are the real ones, with `livemode: false`. 60 loops (patients) and 600 simulations per brand per hour. Idempotent: the same Idempotency-Key answers the first simulation.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
action | "acknowledge" | "accept" | "ship" | "deliver" | "exception" | "policy_withdrawn" | "renewal_due" | yes | `acknowledge`, `accept`, `ship`, `deliver` or `exception`: the sandbox test pharmacy reports the order's status (earlier steps first; the order must have been transmitted; a refill order walks its own steps). `policy_withdrawn`: the regulatory policy of the prescription's regulated substance is withdrawn for this prescription only (held `policy_withdrawn`, `formulary.policy_changed` to you). `renewal_due`: the therapy's check-in falls due now and the renewal notice is requested. |
orderId | string | no | The order to report on; default the prescription's first order. Name a refill's order (`orderId` of the fill) to move it through to delivered. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"object": "sandbox_simulation",
"kind": "case",
"action": "example-action",
"objectId": "example_objectId",
"patientId": "00000000-0000-4000-8000-00000000e001",
"actor": {
"kind": "sandbox_test_clinician",
"label": "example-label"
},
"result": {},
"events": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"type": "example-type",
"object": "example-object",
"objectId": "example_objectId",
"deliveryState": "example-delivery-state",
"recordedAt": "2026-01-01T00:00:00.000Z"
}
],
"standardEvents": [
"example-standard-events"
],
"livemode": false,
"simulatedAt": "2026-01-01T00:00:00.000Z",
"replayed": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 422 | validation_failed | The simulation request is invalid. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 422 | simulation_action_invalid | That action is not one this object takes. |
| 409 | idempotency_key_reused | This Idempotency-Key was used for a different simulation. |
| 429 | sandbox_simulation_rate_limited | At most 600 sandbox simulations per brand per hour. |
| 429 | sandbox_loop_rate_limited | At most 60 sandbox loops (patients) per brand per hour. |
| 503 | simulation_unavailable | The simulation could not run. Nothing changed; retry with the same Idempotency-Key. |
| 404 | prescription_not_found | No prescription with that id for this patient. |
| 409 | order_not_found | The prescription has no pharmacy order yet. |
| 409 | order_not_transmitted | The order has not been transmitted yet; transmission waits on every hold and runs within a minute. |
| 409 | order_closed | The order is closed. |
| 409 | order_past_exception | The pharmacy has already accepted this prescription or refill order. |
| 409 | sandbox_pharmacy_unavailable | The order is routed to a pharmacy that is not the sandbox test pharmacy. |
| 409 | pharmacy_report_refused | The pharmacy status could not be applied in this order's state. |
| 409 | regulated_product_required | policy_withdrawn applies to a prescription of a regulated (policy) substance. |
| 409 | prescription_not_active | The prescription is not active. |
| 413 | payload_too_large | The request body exceeds 2048 bytes (2 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/patients/{patientId}/lab-results/{labResultId}/simulatelabs.writeSandbox: the test clinician signs a lab result off
The sandbox test clinician of the result's case signs it off with the real sign-off command (`clinical.lab.reviewed`); a critical result records the synthetic contact confirmation. The labs gate then clears or holds exactly as it would. Sandbox only: a sandbox phi_server key, a synthetic brand and the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). The actor is synthetic and labelled; the commands, gates and events are the real ones, with `livemode: false`. 60 loops (patients) and 600 simulations per brand per hour. Idempotent: the same Idempotency-Key answers the first simulation.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
action | "sign_off" | yes | `sign_off`: the sandbox test clinician signs the result off. |
disposition | "no_action" | "follow_up" | "repeat_test" | no | Default `no_action` for a normal result, `follow_up` otherwise. A critical result records the synthetic contact confirmation. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"object": "sandbox_simulation",
"kind": "case",
"action": "example-action",
"objectId": "example_objectId",
"patientId": "00000000-0000-4000-8000-00000000e001",
"actor": {
"kind": "sandbox_test_clinician",
"label": "example-label"
},
"result": {},
"events": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"type": "example-type",
"object": "example-object",
"objectId": "example_objectId",
"deliveryState": "example-delivery-state",
"recordedAt": "2026-01-01T00:00:00.000Z"
}
],
"standardEvents": [
"example-standard-events"
],
"livemode": false,
"simulatedAt": "2026-01-01T00:00:00.000Z",
"replayed": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 422 | validation_failed | The simulation request is invalid. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 422 | simulation_action_invalid | That action is not one this object takes. |
| 409 | idempotency_key_reused | This Idempotency-Key was used for a different simulation. |
| 429 | sandbox_simulation_rate_limited | At most 600 sandbox simulations per brand per hour. |
| 429 | sandbox_loop_rate_limited | At most 60 sandbox loops (patients) per brand per hour. |
| 503 | simulation_unavailable | The simulation could not run. Nothing changed; retry with the same Idempotency-Key. |
| 404 | lab_result_not_found | No lab result with that id for this patient. |
| 409 | already_reviewed | This result is already signed off. |
| 409 | case_not_assigned | The case has no clinician yet; the sandbox network assigns one when the case enters review. |
| 409 | sandbox_clinician_unavailable | The case is with a clinician who is not the sandbox test clinician. |
| 409 | sign_off_refused | The sandbox test clinician could not sign the result off. |
| 413 | payload_too_large | The request body exceeds 2048 bytes (2 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/patients/{patientId}/fills/{fillId}/simulatefills.writeSandbox: a refill held too soon becomes due
For a fill held only `refill_too_soon`: every other gate is evaluated for real and, as if enough of the last supply had been used, the real refill order is created (`clinical.fill.requested`, then `clinical.fill.transmitted` when the pharmacy worker sends it). Sandbox only: a sandbox phi_server key, a synthetic brand and the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). The actor is synthetic and labelled; the commands, gates and events are the real ones, with `livemode: false`. 60 loops (patients) and 600 simulations per brand per hour. Idempotent: the same Idempotency-Key answers the first simulation.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
action | "refill_due" | yes | `refill_due`: a fill held only `refill_too_soon` is decided again as if enough of the supply had been used. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"object": "sandbox_simulation",
"kind": "case",
"action": "example-action",
"objectId": "example_objectId",
"patientId": "00000000-0000-4000-8000-00000000e001",
"actor": {
"kind": "sandbox_test_clinician",
"label": "example-label"
},
"result": {},
"events": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"type": "example-type",
"object": "example-object",
"objectId": "example_objectId",
"deliveryState": "example-delivery-state",
"recordedAt": "2026-01-01T00:00:00.000Z"
}
],
"standardEvents": [
"example-standard-events"
],
"livemode": false,
"simulatedAt": "2026-01-01T00:00:00.000Z",
"replayed": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 422 | validation_failed | The simulation request is invalid. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 422 | simulation_action_invalid | That action is not one this object takes. |
| 409 | idempotency_key_reused | This Idempotency-Key was used for a different simulation. |
| 429 | sandbox_simulation_rate_limited | At most 600 sandbox simulations per brand per hour. |
| 429 | sandbox_loop_rate_limited | At most 60 sandbox loops (patients) per brand per hour. |
| 503 | simulation_unavailable | The simulation could not run. Nothing changed; retry with the same Idempotency-Key. |
| 404 | fill_not_found | No fill with that id for this patient. |
| 409 | fill_not_waiting_on_time | Only a fill held refill_too_soon waits on time. |
| 409 | fill_held | The fill is held for another reason now. |
| 409 | fill_refused | The refill order could not be created. |
| 413 | payload_too_large | The request body exceeds 2048 bytes (2 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/patients/{patientId}/notifications/simulatenotifications.ackSandbox: a notice goes unacknowledged past its deadline
A clinically required notice routed to you (or one requested now: `clinical.notification.requested`) passes its acknowledgment deadline; Ambition's content-free fallback becomes due and the notice sender sends it from your domain to the patient's verified contact (`clinical.notification.fallback_sent`). Sandbox only: a sandbox phi_server key, a synthetic brand and the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). The actor is synthetic and labelled; the commands, gates and events are the real ones, with `livemode: false`. 60 loops (patients) and 600 simulations per brand per hour. Idempotent: the same Idempotency-Key answers the first simulation.
Requires an Idempotency-Key header of 16–255 characters. Replays for 24 hours; reusing a key with a different body returns 409 rather than applying the second request.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
action | "notification_unacked" | yes | `notification_unacked`: the notice's acknowledgment deadline passes now; Ambition's content-free fallback becomes due. |
notificationId | string | no | An unacknowledged clinically required notice routed to you. Omit it to request a new one first. |
templateKey | "urgent_clinician_message" | "recall" | "safety_followup" | "rx_cancelled" | "visit_required" | "contact_verification_required" | no | With no notificationId: the clinically required template to request (default `urgent_clinician_message`). |
caseId | string | no | With no notificationId: the patient's case the requested notice is about. |
Example response · 200
{
"data": {
"id": "00000000-0000-4000-8000-00000000e001",
"object": "sandbox_simulation",
"kind": "case",
"action": "example-action",
"objectId": "example_objectId",
"patientId": "00000000-0000-4000-8000-00000000e001",
"actor": {
"kind": "sandbox_test_clinician",
"label": "example-label"
},
"result": {},
"events": [
{
"id": "00000000-0000-4000-8000-00000000e001",
"type": "example-type",
"object": "example-object",
"objectId": "example_objectId",
"deliveryState": "example-delivery-state",
"recordedAt": "2026-01-01T00:00:00.000Z"
}
],
"standardEvents": [
"example-standard-events"
],
"livemode": false,
"simulatedAt": "2026-01-01T00:00:00.000Z",
"replayed": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 422 | validation_failed | The simulation request is invalid. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 422 | simulation_action_invalid | That action is not one this object takes. |
| 409 | idempotency_key_reused | This Idempotency-Key was used for a different simulation. |
| 429 | sandbox_simulation_rate_limited | At most 600 sandbox simulations per brand per hour. |
| 429 | sandbox_loop_rate_limited | At most 60 sandbox loops (patients) per brand per hour. |
| 503 | simulation_unavailable | The simulation could not run. Nothing changed; retry with the same Idempotency-Key. |
| 404 | notification_not_found | No notice with that id for this patient. |
| 404 | case_not_found | No case with that id for this patient. |
| 422 | template_not_clinically_required | Name a clinically required notice template; only those have a fallback. |
| 409 | notification_acknowledged | The notice was already acknowledged. |
| 409 | notification_has_no_fallback | Only an unacknowledged clinically required notice routed to you has a scheduled fallback. |
| 413 | payload_too_large | The request body exceeds 2048 bytes (2 KiB). Nothing was processed; send a smaller body. The edge may refuse it before it reaches the API: the same status and code, with `request_id` `edge` and no `Request-Id` header. |
| 400 | idempotency_key_required | Provide a 16–255 character Idempotency-Key. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with different request parameters. Nothing was applied; use a new key. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still being processed. Retry after `Retry-After`; nothing is applied twice. |
| 503 | idempotency_unavailable | The idempotency record could not be checked. Nothing was processed; retry after `Retry-After`. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/patients/{patientId}/messagespatients.readSandbox: the email Ambition sent your sandbox patient
On the developer sandbox no email is delivered: contact codes (`POST /patients/{patientId}/contact-verifications`) and sign-in links (`POST /sign-in-links`) are caught by the sandbox mail catcher, and this reads them back so your integration can complete the loop. The newest 10 messages Ambition sent this patient, addressed to their email on record, newest first: subject, text (the code or the link is in it), sender and when it was sent. Only your own sandbox patient's mail; the catcher itself is never reachable from outside. Sandbox only: a sandbox phi_server key and a synthetic brand on the developer sandbox deployment (any other answers 403 `sandbox_simulation_only`). 503 `sandbox_mail_catcher_unavailable` when the deployment has no catcher.
Example response · 200
{
"data": [
{
"id": "example_id",
"object": "sandbox_message",
"subject": "example-subject",
"text": "example-text",
"from": {
"name": "example-name",
"address": "example-address"
},
"sentAt": "2026-01-01T00:00:00.000Z",
"livemode": false
}
],
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 400 | patient_context_required | Send Ambition-Patient-Session and Ambition-Patient-Auth. |
| 403 | browser_origin_refused | Patient endpoints are server-to-server only. |
| 403 | phi_key_required | Patient endpoints take a phi_server key. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 404 | patient_not_found | No patient with that id is available to this key. |
| 503 | sandbox_mail_catcher_unavailable | The sandbox mail catcher is not configured on this deployment or did not answer. Nothing was read; retry. |
| 401 | patient_sign_in_unverified | Ambition-Patient-Auth: otp was sent, but Ambition-Patient-Session is not the sessionReference of a current Ambition sign-in for this patient from this key (POST /sign-in-links/{signInLinkId}/confirm answers it; it lasts 24 hours, until signed out or the patient's email is replaced). otp is only valid with an Ambition sign-in session: sign the patient in again. Nothing was processed. |
/api/v1/sandbox/certificationSandbox: your integration certification checklist
The checklist required before live patient data access, computed from your brand's real sandbox activity: clinical receiver verified, signature checked (a mis-signed probe refused), render receipt accepted, consent and intake complete, identity verification, verified contact, labs (when a program requires them), brand release (when a program uses it), fills, renewals, refundExpected handled (merchant-of-record programs), policy_withdrawn handled (regulated programs) and idempotency keys used. Counts and times only; no patient. Any sandbox phi_server key; 403 `sandbox_simulation_only` otherwise.
Example response · 200
{
"data": {
"object": "sandbox_certification",
"environment": "sandbox",
"livemode": false,
"certified": false,
"completeCount": 1,
"requiredCount": 1,
"sandboxPatients": 1,
"items": [
{
"key": "receiver_verified",
"state": "complete",
"count": 1,
"lastAt": "2026-01-01T00:00:00.000Z",
"reason": "example-reason"
}
],
"evaluatedAt": "2026-01-01T00:00:00.000Z"
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 403 | phi_key_required | Only a phi_server key reads the certification. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 503 | certification_unavailable | The checklist could not be read. Nothing changed; retry. |
/api/v1/sandbox/webhook-signature-probesSandbox: send your clinical receivers a mis-signed probe
Queues one signature probe to each verified clinical receiver. The probe is your receiver's own verification envelope (`webhook.verification`, a fresh challenge that completes nothing), sent twice: first signed wrongly on purpose, which your receiver must refuse with 401 or 403, then signed correctly, which it must answer with a 2xx. Both together complete `signature_checked`. A 2xx to the mis-signed copy fails it (`signature_not_checked`); any other answer is `probe_inconclusive` (408 and 429 are retried). At most one open probe per receiver and 20 per hour. Any sandbox phi_server key.
Example response · 202
{
"data": {
"object": "signature_probe",
"sent": 1,
"alreadyOpen": 1,
"receivers": 1,
"livemode": false
},
"meta": {
"requestId": "example_requestId",
"apiVersion": "example-api-version",
"environment": "example-environment"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | unauthenticated | Missing or unrecognised credential. |
| 403 | insufficient_scope | The credential lacks the required scope. |
| 429 | rate_limited | Too many requests, per network address and per API key. Retry after the interval in `Retry-After`; every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. |
| 503 | rate_limit_unavailable | Request protection is temporarily unavailable. Nothing was processed; retry after `Retry-After`. |
| 403 | phi_key_required | Only a phi_server key sends probes. |
| 403 | sandbox_simulation_only | Sandbox simulation runs only on the developer sandbox, for a synthetic brand, with a sandbox key. |
| 409 | receiver_not_verified | Verify a clinical receiver first; the probe goes only to verified receivers. |
| 429 | signature_probe_rate_limited | At most 20 signature probes per brand per hour. |
| 503 | certification_unavailable | The probe could not be queued. Nothing changed; retry. |
Webhook events
Register receivers per connection and environment in the console. Every delivery is signed (ambition-signature, verified as the webhooks guide shows), retried with backoff, and dead-lettered for an operator when it keeps failing. Deduplicate on ambition-event-id: it is the same on every attempt, at every receiver and in every replay.
| Event | Sent when | Status |
|---|---|---|
order.acknowledged | Order acknowledged | Planned: nothing sends it yet |
order.accepted | Order accepted | Planned: nothing sends it yet |
order.in_production | Order in production | Planned: nothing sends it yet |
order.shipped | Order shipped | Planned: nothing sends it yet |
order.delivered | Order delivered | Planned: nothing sends it yet |
order.exception | Order exception | Planned: nothing sends it yet |
payment.captured | A patient payment was captured | Has a producer; sent where webhooks run |
payment.failed | A patient payment failed | Has a producer; sent where webhooks run |
subscription.created | A patient subscription was created | Has a producer; sent where webhooks run |
subscription.updated | A patient subscription changed status | Has a producer; sent where webhooks run |
subscription.canceled | A patient subscription was canceled | Has a producer; sent where webhooks run |
enrollment.created | An enrollment was created | Has a producer; sent where webhooks run |
enrollment.status_changed | An enrollment's coarse status changed. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.patient.created | Patient created. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.patient.updated | Patient updated. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.patient.deactivated | Patient deactivated. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.contact.verified | Contact verified by Ambition. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.identity_verification.completed | Identity verified. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.identity_verification.failed | Identity verification failed. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.identity_verification.review_required | Identity verification needs review. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.referral.accepted | The medical group accepted a referral. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.referral.declined | The medical group declined a referral. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.referral.expired | A referral expired without its next step. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.document.available | A patient document is available. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.consent.recorded | A consent was recorded. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.consent.withdrawn | A consent was withdrawn. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.intake.submitted | An intake was submitted. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.intake.identity_required | A case waits for identity verification. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.intake.queued | A waiting case entered the clinician queue. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.intake.expired | A waiting case closed unverified. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.safety_report.received | A safety report was received. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.fill.requested | A brand-requested fill was sent toward the pharmacy. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.fill.held | A brand-requested fill is held. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.fill.transmitted | A fill reached the pharmacy. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.prescription.refills_exhausted | A prescription has no refills left. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.renewal.requested | A renewal request opened a case. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.checkin.due | A therapy check-in is due. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.monitoring.labs_due | Monitoring labs are due. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.created | A case was created. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.assigned | A clinician was assigned. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.needs_info | The clinician needs more information. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.info_received | The patient's answer reached the clinician. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.labs_required | The clinician needs labs. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.approved | The clinician approved. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.declined | The clinician declined. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.cancelled | The request was cancelled. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.case.closed | The case was closed by the care team. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.prescription.signed | A prescription was signed. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.prescription.held | A prescription is held. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.prescription.released | A prescription hold was released. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.prescription.cancelled | A prescription was cancelled. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.created | A pharmacy order was created. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.acknowledged | The pharmacy acknowledged the order. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.accepted | The pharmacy accepted the order. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.in_production | The order is being prepared. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.shipped | The order shipped. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.delivered | The order was delivered. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.order.exception | The order has a problem. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.message.created | A message was posted. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.lab.result_received | A lab result was received. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.lab.critical | A lab result has a critical value. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.lab.reviewed | A lab result was signed off. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.lab.order_signed | A lab order was signed. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.lab.order_updated | A lab order's status changed. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.lab.released | A lab result was released to the patient. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.notification.requested | A care notice is due to your patient. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
clinical.notification.fallback_sent | Ambition sent your patient a content-free fallback notice. Needs the business associate agreement and downstream-systems attestation | Has a producer; sent where webhooks run |
formulary.policy_changed | A regulatory policy your catalog relies on changed | Has a producer; sent where webhooks run |
formulary.state_availability_changed | A program's State availability changed | Has a producer; sent where webhooks run |
The webhooks guide has the signed string, verifiers in Python, Go, Ruby and Node, the retry schedule and every event’s payload.
A commerce or enrollment event’s data holds only ids (with the enrollment id and your externalReference), the Stripe object id, the amount in cents, the currency, a coarse status and the time it happened. It never carries a patient’s name or contact details, a product, a program or anything clinical. payloadDigest is the SHA-256 of that payload as recorded.
Versioning and deprecation
Additive changes — a new endpoint, a new optional field, a new enum member in a response — ship without a version bump. A breaking change is announced on the changelog, marked deprecated in the specification with a Sunset date at least 180 days out, and only then removed.
Errors always have the same shape: an error object with a stable code, a human message, and optional per-field detail. Branch on code, never on message.