Developer guide
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.
Open the Developer CenterNever send patient information to a sandbox key.
On this deployment: Headless clinical API (patient data): Not available · Webhooks to your receivers: Sandbox only
- 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).
Before you start
This is headless mode: your app runs the patient experience and your Node server calls the patient API. Everything below runs on the developer sandbox deployment, on synthetic data. Use the SDK's headless() client or plain HTTPS.
Status: written, not open. The headless patient API exists on the developer sandbox build and has run only against synthetic data; no brand's patient data access is approved in any environment yet, and there are no live PHI keys.
1. Get patient data access, then mint a sandbox PHI server key
In the console, Developers > Patient data access walks your brand through the gate: the BAA with the physician group that covers your patients, your privacy officer's attestation that your PHI pages carry no ad pixels or third-party scripts, the platform PHI terms, your downstream vendor inventory, on real data your verified sending domain (Developers > Contact sender), and a network administrator's approval. Then an owner or admin mints a phi_server key (Developers > PHI server keys) with an IP allowlist; it expires within 90 days and rotates with a 7-day overlap. Keep it on your server only.
Sandbox: On the sandbox the key is ak_sandbox_… and reaches synthetic patients only, and no sending domain is needed.
GET /phi-access/status
2. Create the patient from your own sign-up
Your app owns the account. When a person signs up, your server creates their Ambition patient with your own external id and the demographics you collected, and keeps the patientId. Every patient call after this sends Ambition-Patient-Session and Ambition-Patient-Auth: your opaque session reference and how the person signed in to you. Never call from a browser: a request with an Origin header is refused.
POST /patientsGET /patients/{patientId}
Events: clinical.patient.created.
3. Verify identity with your own IDV vendor
Run government photo ID plus selfie liveness at your allowlisted vendor (Persona, Vouched or Incode) with reference_id = the patientId, then submit {mode: byo, vendor, inquiryId}. Ambition fetches the result from the vendor with the read-only credential you registered and checks the binding, timing, template, uniqueness and a name and date-of-birth match; your assertion of identity never counts. Without a vendor, {mode: ambition} returns a token for the vendor SDK you embed. Identity is not needed to submit intake: the case waits in awaiting_identity.
Sandbox: On the sandbox use vendor sandbox_fixture; no real IDV vendor is contacted.
POST /patients/{patientId}/identity-verificationsGET /patients/{patientId}/identity-verifications/current
Events: clinical.identity_verification.completed.
4. Verify one contact (your sign-up code)
Ambition sends the code from your own domain, so it can be your sign-up one-time code and the person sees no extra step. It proves control of the email; it is not an Ambition sign-in, so afterwards send Ambition-Patient-Auth with the method your own sign-in used, not otp (otp is accepted only with a sign-in link's sessionReference). One verified contact is needed before the first prescription is transmitted (hold reason contact_unverified), not before intake. A phone your IDV vendor attests also counts. SMS waits on per-brand 10DLC registration: email only at launch.
Sandbox: On the sandbox the email is caught by the sandbox mail catcher, never delivered to a real inbox: read the code with GET /sandbox/patients/{patientId}/messages (patients.read).
POST /patients/{patientId}/contact-verificationsPOST /patients/{patientId}/contact-verifications/{verificationId}/confirmGET /sandbox/patients/{patientId}/messages
Events: clinical.contact.verified.
5. Present the group's consents and record acceptance with a render receipt
The physician group issues the telehealth consent, the NPP acknowledgment and the compounded-medication notices. Ask for a presentation (a single-use nonce per document), render the text verbatim with a separate, unchecked checkbox per scope, and record acceptance with the render receipt: the sha256 of the text and the label you rendered, each checkbox's event time and the locale. A receipt that does not match is refused (rendered_text_mismatch, label_mismatch) and nothing is recorded.
GET /catalog/{brand}/consent-documentsPOST /patients/{patientId}/consent-presentationsPOST /patients/{patientId}/consents
6. Run the questionnaire and submit intake
The questionnaire is FHIR R4 Questionnaire JSON, so a FHIR form renderer works. Save drafts as the person answers, evaluate for knockouts, then submit QuestionnaireResponse items with the declared location. The submission opens a case. Until identity is verified it parks in awaiting_identity (it closes after 14 days, and you refund under your own terms); once verified it enters the clinician queue.
GET /catalog/{brand}/programs/{handle}/questionnairePUT /patients/{patientId}/intake-drafts/{programHandle}POST /patients/{patientId}/intake/evaluatePOST /patients/{patientId}/intake-submissions
Events: clinical.case.created, clinical.case.assigned.
7. Send the lab results the program requires
When a program needs labs before prescribing, the case says so. Submit structured LOINC observations with the performing lab's CLIA number and the collection time; a clinician reviews and signs them off, and a critical value is routed to the care team. Labs that are due also hold fills.
Sandbox: Ask the sandbox test clinician to sign a result off: POST /sandbox/patients/{patientId}/lab-results/{labResultId}/simulate with action sign_off runs the real sign-off (a critical result with the synthetic contact confirmation), so clinical.lab.reviewed reaches your receiver and the labs gate clears or holds exactly as it would.
GET /patients/{patientId}/lab-requirementsPOST /patients/{patientId}/lab-resultsPOST /sandbox/patients/{patientId}/lab-results/{labResultId}/simulate
Events: clinical.case.labs_required, clinical.lab.result_received, clinical.lab.reviewed, clinical.lab.critical.
8. Receive the decision and read the prescription
A licensed clinician of the physician group decides. Webhooks carry refs only: on clinical.prescription.signed, read the prescription with your key. clinical.case.declined, clinical.case.closed and clinical.intake.expired carry refundExpected (as the case's decision.refundExpected does): true when you are merchant of record, and then the platform terms require a full refund of the medication charge. clinical.document.available names a new document (a signed prescription, a decided care record, a signed-off lab report): fetch it by that id. Answer a more-info request with the patient's own words.
Sandbox: The developer sandbox has no clinicians, pharmacies or shipments of its own; its synthetic test clinician acts for you. POST /sandbox/patients/{patientId}/cases/{caseId}/simulate with action approve, needs_info or decline runs the real decision on your sandbox case (approve also writes, signs and routes the prescription through the real rail), so clinical.case.approved, clinical.prescription.signed and clinical.order.created reach your sandbox receiver with livemode: false. Every real gate holds: a case still waiting on labs answers 409 labs_required. Or click Approve in Developers > Clinical loop simulator.
GET /patients/{patientId}/cases/{caseId}GET /patients/{patientId}/prescriptions/{prescriptionId}POST /sandbox/patients/{patientId}/cases/{caseId}/simulate
Events: clinical.case.approved, clinical.prescription.signed, clinical.case.needs_info, clinical.case.declined, clinical.document.available.
9. Capture payment, then release fulfillment
You are the merchant of record (billing mode B): you show one price and collect it; Ambition bills you fixed fees per review and per fill, never a percentage. With transmission_release = brand_release the prescription waits until you release it after capturing payment; unreleased after 7 days, it is not transmitted. A release never overrides a clinical or regulatory hold (for example policy_withdrawn).
Sandbox: Stripe runs in test mode only; no money moves. Once the order is transmitted to the sandbox test pharmacy, POST /sandbox/patients/{patientId}/prescriptions/{prescriptionId}/simulate with action ship or deliver sends the pharmacy's real status callbacks (clinical.order.shipped, clinical.order.delivered).
POST /patients/{patientId}/prescriptions/{prescriptionId}/release-fulfillment
Events: clinical.prescription.released, clinical.order.created, clinical.order.shipped.
10. Drive the refills from your subscription
The first dispense is the prescription's own order (released with release-fulfillment under brand_release); request fills from the second cycle on. A request before enough of the last supply is used is held refill_too_soon. Each billing cycle, request a fill with your cycle reference and the location the patient declares. Ambition checks refills left, expiry, check-ins and labs due, State coverage and regulatory standing, then sends the pharmacy order or holds it with a reason. To pause or cancel, do not request the next fill.
Sandbox: A fill requested right after the first dispense is held refill_too_soon. POST /sandbox/patients/{patientId}/fills/{fillId}/simulate with action refill_due decides it again as if enough of the supply had been used (every other gate is real) and creates the real refill order; renewal_due on the prescription makes the check-in and the renewal notice due now.
POST /patients/{patientId}/prescriptions/{prescriptionId}/fillsGET /patients/{patientId}/orders/{orderId}POST /sandbox/patients/{patientId}/fills/{fillId}/simulate
Events: clinical.fill.requested, clinical.fill.transmitted, clinical.fill.held.
11. Receive clinical webhooks and acknowledge notices
Verify every delivery's signature before acting, persist the event id and treat a repeat as a duplicate, and apply an event only when its sequence is higher than the last you applied for that object. When a care notice is due, send your own email, SMS or push and acknowledge it; a clinically required notice that is not acknowledged in time is followed by a content-free notice Ambition sends from your domain. Backfill gaps with GET /events. If your patient data access closes, your receivers are disabled and clinical events are held for up to 30 days. Once it reopens, GET /events serves them at once; verify each receiver again and it then gets what it missed, under the same event ids.
Sandbox: Sandbox events carry livemode: false. POST /sandbox/patients/{patientId}/notifications/simulate passes a clinically required notice's deadline so you can see the fallback. GET /sandbox/certification lists what your integration has shown in the sandbox, item by item, before live patient data keys.
GET /eventsPOST /patients/{patientId}/notifications/{notificationId}/deliveredPOST /sandbox/patients/{patientId}/notifications/simulateGET /sandbox/certification
Events: clinical.notification.requested, clinical.notification.fallback_sent.