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

GET/api/v1/formularyno auth

List 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

FieldTypeRequiredNotes
status"approved" | "approved_off_label" | "compoundable" | "conditional" | "prohibited"no
categorystringno
searchstringno
limitintegerno
cursorstringno

Errors

StatusCodeWhen
422validation_errorQuery parameters were invalid.
GET/api/v1/formulary/{slug}no auth

Read 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

StatusCodeWhen
404not_foundNo substance matches that slug or alias.
GET/api/v1/rulebookno auth

Read 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

StatusCodeWhen
503rulebook_unavailableNo active rulebook is loaded.
GET/api/v1/statusno auth

Service 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

StatusCodeWhen
503status_unavailableStatus could not be read.

Authenticated

POST/api/v1/claims/scanclaims.scan

Evaluate 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

FieldTypeRequiredNotes
textstringyesMarketing copy to evaluate.
context"landing_page" | "email" | "sms" | "paid_ad" | "social" | "packaging"no

Errors

StatusCodeWhen
401unauthenticatedMissing or unrecognised credential.
403insufficient_scopeThe credential lacks the required scope.
429rate_limitedToo many requests. Retry after the interval in `Retry-After`.
422validation_errorRequest body did not match the contract.
503rulebook_unavailableNo active rulebook is loaded.
GET/api/v1/deliveriesdeliveries.read

Every 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

FieldTypeRequiredNotes
status"delivered" | "retrying" | "dead_letter" | "duplicate" | "reconciled" | "rejected"no
orderIdstringno
eventTypestringno
limitintegerno

Errors

StatusCodeWhen
401unauthenticatedMissing or unrecognised credential.
403insufficient_scopeThe credential lacks the required scope.
429rate_limitedToo many requests. Retry after the interval in `Retry-After`.
422validation_errorQuery parameters were invalid.
POST/api/v1/deliveries/replaydeliveries.replay

Re-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

FieldTypeRequiredNotes
outboxIdstringyes
reasonstringyesRecorded against the replay. Shown in the delivery history.

Errors

StatusCodeWhen
401unauthenticatedMissing or unrecognised credential.
403insufficient_scopeThe credential lacks the required scope.
429rate_limitedToo many requests. Retry after the interval in `Retry-After`.
404not_foundNo such delivery for this brand.
422validation_errorRequest body did not match the contract.
POST/api/v1/pharmacy/callbacks/{partnerId}partner signature

Receive 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

FieldTypeRequiredNotes
eventIdstringyes
eventType"prescription.status" | "prescription.clarification" | "fulfillment.status"yes
schemaVersionstringyes
orderIdstringyes
partnerReferencestringyes
status"acknowledged" | "clarification_required" | "accepted" | "rejected" | "in_production" | "shipped" | "delivered" | "canceled"yes
occurredAtstringyes
reasonCodeanyno
questionanyno

Errors

StatusCodeWhen
400invalid_jsonThe callback body is not valid JSON.
401invalid_callback_signatureThe HMAC or timestamp window is invalid.
404callback_contract_unavailableNo active callback contract matches the partner.
413payload_too_largeThe callback exceeds 256 KiB.
422callback_validation_failedThe body, event identity, schema, time, or order tuple is invalid.
429rate_limitedThe public API callback budget is exhausted.
503callback_store_unavailableThe durable callback store is unavailable.
POST/api/v1/sandbox/orderssession

Create 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

FieldTypeRequiredNotes
programKeystringyes
skustringyes
jurisdictionstringyes
visitMode"async" | "video"yes

Errors

StatusCodeWhen
400idempotency_key_requiredProvide a 16–255 character Idempotency-Key.
403forbiddenTenant integration access is required.
409idempotency_conflictKey reused with different parameters.
422validation_errorRequest 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.