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 (
SEMA-1M) 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
checkout-first buy link and the prospect-capture call expect, not the merchandising or checkout
call.
Your welcome kit lists each offering’s offering_ref and sku_id. A checkout-first buy link also
carries the offering’s therapy, which your onboarding contact gives you. The example used across
these pages is one offering’s three values:
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
an x-held-capability header proving the caller holds the enrollment it names, and the only body
keys it accepts are offering_ref, journey_id, and an optional promo. Only the hosted
checkout sends that capability, so leave this create to the hosted checkout (see
the checkout door).
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. - On
POST /v1/payments/checkout-sessions, a404with typeabout:blankand the detailjourney not foundmeans the create did not prove that it holds the enrollment named by itsjourney_id: thex-held-capabilityheader was missing, or did not match that enrollment and brand. The answer is the same whether or not the enrollment exists. Only the hosted checkout sends a capability, so hand the patient there rather than creating the session yourself (see the checkout door).
401 before the
path is routed, so a wrong path sent without a key returns that 401 rather than the generic 404.