API reference · version 2026-08-22
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 at /api/v1/openapi is generated from the same schemas the handlers validate with, so it cannot describe a shape the API does not enforce.
Sixty seconds
# The formulary needs no credential at all.
curl https://ambitionhealth.co/api/v1/formulary?status=prohibited
# Ask whether one substance is sellable, and why not.
curl https://ambitionhealth.co/api/v1/formulary/bpc-157
# Scan copy against the same rulebook that gates publication.
curl -X POST https://ambitionhealth.co/api/v1/claims/scan \
-H "authorization: Bearer $AMBITION_API_KEY" \
-H "content-type: application/json" \
-d '{"text":"Same active ingredient as Ozempic."}'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.
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 | |
category | string | no | |
search | string | no | |
limit | integer | no | |
cursor | string | no |
Errors
| Status | Code | When |
|---|---|---|
| 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.
Errors
| Status | Code | When |
|---|---|---|
| 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.
Errors
| Status | Code | When |
|---|---|---|
| 503 | rulebook_unavailable | No active rulebook is loaded. |
/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.
Errors
| Status | Code | When |
|---|---|---|
| 503 | status_unavailable | Status could not be read. |
Authenticated
/api/v1/claims/scanclaims.scanEvaluate marketing copy against the rulebook before you publish it
Runs the same engine, against the same rulebook, that gates publication inside the platform — so a result here is the result you will get at publish time. Each hit carries the matched text, the provision behind the rule, and a compliant rewrite where one exists. Scanning does not store your copy against a brand unless the credential is brand-scoped.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | Marketing copy to evaluate. |
context | "landing_page" | "email" | "sms" | "paid_ad" | "social" | "packaging" | no |
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. Retry after the interval in `Retry-After`. |
| 422 | validation_error | Request body did not match the contract. |
| 503 | rulebook_unavailable | No active rulebook is loaded. |
/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, and when the next attempt is due.
Query parameters
| Field | Type | Required | Notes |
|---|---|---|---|
status | "delivered" | "retrying" | "dead_letter" | "duplicate" | "reconciled" | "rejected" | no | |
orderId | string | no | |
eventType | string | no | |
limit | integer | no |
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. Retry after the interval in `Retry-After`. |
| 422 | validation_error | Query parameters were invalid. |
/api/v1/deliveries/replaydeliveries.replayRe-send a delivery that failed, without opening a support ticket
Queues a fresh attempt at the same event. The request id and payload digest are preserved, so a partner honouring idempotency will not create a second order. The reason is recorded against the replay and appears in the delivery history.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
outboxId | string | yes | |
reason | string | yes | Recorded against the replay. Shown in the delivery history. |
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. Retry after the interval in `Retry-After`. |
| 404 | not_found | No such delivery for this brand. |
| 422 | validation_error | Request body did not match the contract. |
/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.
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" | yes | |
occurredAt | string | yes | |
reasonCode | any | no | |
question | any | no |
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. |
| 413 | payload_too_large | The callback exceeds 256 KiB. |
| 422 | callback_validation_failed | The body, event identity, schema, time, or order tuple is invalid. |
| 429 | rate_limited | The public API callback budget is exhausted. |
| 503 | callback_store_unavailable | The durable callback store is unavailable. |
/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.
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 | |
sku | string | yes | |
jurisdiction | string | yes | |
visitMode | "async" | "video" | yes |
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. |
| 422 | validation_error | Request body did not match the contract. |
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.