application/problem+json —
your integration meets along the way.
Identifiers
Every offering has two machine identifiers. The catalog console shows both on an offering’s detail screen, each one copyable and labeled by the call that accepts it — so you never have to guess one or compose it by hand.
The code you read on an offering screen (
SYN-TRZ-3M) is the offering_ref — there is no
separate lookup, and no display-versus-machine split for it in this version. Pass that code wherever
an offering_ref is asked for. The sku_id is a different string (sku_…); it is the id the
prospect-capture call expects, not the merchandising or checkout call.
Of these calls, POST /v1/payments/checkout-sessions and GET /v1/payments/{order_ref}/status
appear on the API reference tab. The merchandising read and prospect capture are not in the published
reference: prospect capture (POST /v1/prospects) is made by the platform’s own hosted checkout on
its server, so your storefront never calls it directly.
The enrollment thread
A patient’s enrollment carries ajourney_id (shaped jny_…), minted the first time an intake
session resolves. On the hosted platform it travels in the X-Journey-Id request header — that
header is how the platform threads one patient’s context across calls. Send it back on a later
GET /v1/instrument/resolve to pick an in-progress questionnaire up where the patient left off,
and it is the same id you see on webhook events and on the enrollment status read
(GET /v1/journeys/{journey_id}/status). The X-Journey-Id header is what the hosted platform
reads to resume; a journey_id query parameter is honored only by the standalone renderer used for
direct testing, so build against the header.
Problem responses
Every refusal is anapplication/problem+json body: a stable type, a short title, and the HTTP
status repeated in the body. Switch on the type — it is the stable, machine-readable part.
The API-key wall
The platform’s key-protected reads — for example, your account’s orders — sit behind your API key in anAuthorization: Bearer header, scoped to a brand you own with X-Brand-Id:
- A request with no key is refused
401https://purple.md/problems/unauthenticated. - A valid key paired with a brand that is not yours is refused
404https://purple.md/problems/brand-not-found— never403, so a brand’s existence never leaks across tenants.
The typed responses you will meet
The checkout create can also return
commerce/checkout/offering-not-purchasable (422) — the
offering resolved but has no usable price binding.
The checkout create requires an Idempotency-Key request header of at least eight characters, and
the only body keys it accepts are offering_ref, journey_id, and an optional promo.
One more typed response comes from the enrollment status read rather than checkout:
That read can also return
503: the status source was unavailable, so the read did not succeed.
That tells you nothing about the enrollment itself — retry with backoff, and do not read a failed
call as a statement about the patient. Both sit alongside the 400/401/404 refusals above, not
in place of them.
When a refusal is not namespaced
Not every refusal carries a namespacedtype. Some the platform makes before it reaches the
checkout logic — a request whose body is not valid JSON, or one missing its X-Brand-Id — come
back with the RFC 7807 default type of about:blank, still carrying a title, the HTTP
status, and a detail. So switch on type when it is namespaced, and fall back to the HTTP
status when it is about:blank. The list above is the set of named types you will meet; treat it
as the ones worth branching on, not as an exhaustive catalog of every possible refusal.
Reading a 404
Not every404 means the same thing:
- A
404whosetypeis namespaced (commerce/…) means your call reached the platform and the resource is mounted but not configured — fix your configuration or your pricing, not your URL. - A
404with the generic typehttps://purple.md/problems/not-foundmeans the path itself is not a routable resource — check the URL and the/v1prefix.
401 before the
path is routed, so a wrong path sent without a key returns that 401 rather than the generic 404.