Skip to main content
Everything a returning integrator needs to know about what changed since they last built against the platform. Filter with the tag buttons: API marks a change to the reference surface, Behavior a change to how an endpoint acts, Docs a guide or clarification. Breaking means you must act; Additive means nothing you already built will break. One timeline, newest first. Entries tagged API are generated automatically from a diff of the published OpenAPI contract — they appear here when a release carrying the change reaches the test environment, so the reference matches the newest staging release, with each operation linking to its page in the API reference. Guide and behavior notes are authored alongside the change that ships them. Both kinds share a single reverse-chronological list, so the most recent change is always at the top.
APIAdditive
commit 37f1a69
AddedChanged
APIBreaking
commit 1ef2ff2
Changed
BehaviorAdditive
You choose each offering's billing cadence
You can now choose how members are billed for each of your offerings. PUT /v1/account/brands/{brand_id}/offerings/{offering_ref}/billing-cadence takes {"billing": "monthly"} or {"billing": "upfront"}. Only a cadence that suits the offering’s supply is accepted, and a refusal lists the cadences you can choose. The shipping cadence always follows the offering’s supply and is never set by you.When only one billing cadence suits an offering, the platform sets it for you when the offering is enabled and already has its copy, or when its copy is adopted, whichever comes first, so most offerings need no call. Your readiness report now shows a cadence_compatible check per offering. Previously billing terms were confirmed by your onboarding contact; see Before a price is payable.
APIAdditive
commit 5143bf5
Added
APIAdditive
commit 84305e5
Changed
DocsBreaking
The test environment's hosts, in one place
The test environment has moved to new hosts, and the guides named the old API base. Use the PURPLE_API_BASE value your welcome kit states: your key works only on the environment it was issued for. The API reference’s playground now sends its requests to the same test environment.Environments now lists the test environment’s hosts in one place: the API base, your member portal address and the hosted sign-in page. It also says that custom sign-in domains are not live in the test environment; see Login URLs and domains. Every example still reads its base from $PURPLE_API_BASE.
APIAdditive
commit bc92e7c
Changed
  • GET /v1/account/brands/{brand_id}/storefront — getCommerceAccountBrandStorefront: response field added: data.pricing.offering.renewal_interval_days (optional); response field added: data.pricing.offering.shipping_interval_days (optional)
BehaviorAdditive
Your API key can read your own onboarding progress
GET /v1/onboardings/{onboarding_id}/progress is now callable with your API key, for your own onboarding. Send your key and X-Brand-Id set to one of your brands. The answer says which step you are on out of how many, what is waiting on you, and, when your onboarding has a LegitScript step, each update’s date and meaning.Another client’s id answers 404, exactly as an id that does not exist, so the API never confirms whether another client’s onboarding exists.This replaces the 2026-10-10 note below: you no longer need to remove the call or ask your onboarding contact. See the Onboarding timeline.
APIAdditive
commit c8f531e
Changed
APIAdditive
The onboarding progress read now carries the LegitScript step's timeline
The onboarding progress answer can now show where your LegitScript certification stands. GET /v1/onboardings/{onboarding_id}/progress gains an optional legitscript_timeline object. It is present when your onboarding includes the LegitScript step, and absent otherwise.
  • state is ok when the certification record was read, or unavailable when it could not be read right now. An unavailable timeline has no entries; read again later.
  • entries lists every recorded update, oldest first. Each has at, the time of the update, and meaning, what it means in plain words, such as an application status change or a certification granted or lost. There are no vendor status codes and no staff notes.
The field appears only in the 200 answer of this read. Nothing you already built changes.
APIAdditive
Two more therapies in the qualification fact list
The therapy filter accepts two more values. GET /v1/account/question-library/facts now accepts therapy=anti_aging_nad_injection and therapy=pt_141. Every value it accepted before still works. A therapy appearing in this list says what you can author questions for. It is never a statement of medical readiness.
APIAdditive
The catalog error body declares an optional adoption summary
The error body shared by your pricing and starter-merchandising writes declares a new optional field, adoption. It summarises a staff catalog import and is set only on refusals from staff catalog-mapping writes. An error from PUT /v1/account/brands/{brand_id}/pricing or POST /v1/account/brands/{brand_id}/starter-merchandising does not carry it. Nothing you already built changes.
APIAdditive
Qualification authoring refusals can carry a finer code
A refusal from the qualification authoring operations can now carry an optional code. It appears only when a refusal shares its type with another condition and needs a finer name, for example decision.bundle_intake_disagrees or decision.bundle_unknown. Branch on type first and read code only to tell those apart. This applies to every question-library and offering qualification operation, such as POST /v1/account/question-library/questions and POST /v1/account/offerings/{offering_ref}/qualification/publish.
APIAdditive
Your brand can show a support phone number
Your brand’s display identity takes an optional support_phone. PUT /v1/account/brands/{brand_id}/config accepts display.support_phone, written as it should be displayed: digits, with an optional leading +, spaces, parentheses, dots and hyphens, at most 32 characters. A malformed value is refused with 422. The member support page offers it as a one-tap call. Both that write and GET /v1/account/brands/{brand_id}/config return it as data.display.support_phone. Leave it out and nothing changes.
APIAdditive
Qualification authoring refusals say more, as data
Refusals from the qualification authoring operations name more of what went wrong as data, so you can word them without parsing message. All new fields are optional, and message is unchanged.
  • required_role, on a role refusal (403): the least role that may make the write, admin or staff.
  • issues[].already_asked, on question.fact_already_asked and question.ask_again_alias_only: the question that already asks the fact, with its section and the prompt a member sees.
  • issues[].missing_values, on question.ask_again_options_incomplete when the fact is a list of options: the option codes your question does not offer. When the fact is free text, a number or a date, issues[].value_type names its type instead.
  • issues[].refresh_policy, on question.ask_again_suppressed and question.ask_again_collects_blank: the fact’s refresh policy, which says why the question is not shown as a confirm.
This applies to every question-library and offering qualification operation, such as PATCH /v1/account/question-library/questions/{question_id} and PATCH /v1/account/offerings/{offering_ref}/qualification/questions/{question_id}.
APIAdditive
Intake questions can carry a unit; authoring refusals name who changed the intake first
A question can now show the unit its answer is in. The question node returned by GET /v1/instrument/resolve, POST /v1/instrument/next and POST /v1/instrument/abandon can carry an optional node.unit, for example in or lb. Show it beside the prompt. When it is absent, show no unit and never guess one. It is display text only.Qualification authoring refusals say more, as data. All new fields are optional:
  • landed_first, on a stale-edit conflict (409): the change that landed before yours, with who made it, when, and what it was. Its action can be set_create, set_rename, set_remove, set_move, question_add, question_edit, question_remove or question_reorder. When source is current_pointer, only the most recent author is known.
  • issues[].readers, on remove.answer_has_readers and when.answer_has_readers: the rule, condition or display that still reads the answer, as a kind and an id you can look up in the content you already hold.
APIAdditive
commit 5dcce7b
Changed
BehaviorBreaking
Starting a checkout session now requires proof that the caller holds the enrollment
Naming an enrollment is no longer enough to buy against it. POST /v1/payments/checkout-sessions now refuses any create that does not prove it holds the enrollment named by its journey_id. The proof is the enrollment’s capability, sent on the new x-held-capability request header. Only the hosted checkout sends it: on a checkout-first offering it holds the capability POST /v1/prospects returned to its own server, and on a questions-first offering it holds the one your storefront handed over through the form POST to the checkout door.A create without a capability, or with one that does not match that enrollment and brand, is refused with 404 and this body:
The answer is the same whether the enrollment exists, belongs to another patient, or does not exist. Nothing is created, no payment is attempted, and your Idempotency-Key is not used up. Every create carries a journey_id, so every create needs the capability. No create is exempt.This change does not alter the hosted checkout. Where the hosted checkout holds the capability, it sends it on the patient’s behalf.What you must do: if your server calls POST /v1/payments/checkout-sessions with a bare journey_id, that call now answers 404. Remove it and let the hosted checkout create the session: link a checkout-first patient to it, and hand a questions-first patient over with the form POST described in handing the patient to checkout. See also the checkout door and pricing.
BehaviorBreaking
Questions-first storefronts hand off to checkout with a form POST
A questions-first checkout no longer opens from a link carrying journey_id. Naming an enrollment does not prove it belongs to the patient in the browser, so the checkout refuses it. Instead:
  • the POST /v1/instrument/next response whose answer completes the questionnaire now carries destination_capability, a one-time checkout pass that lasts about 10 minutes. It is returned only to a session your server started fresh with your secret key, and never on a resumed session. A later call on a session that is already complete returns none. If the platform redelivers the completion internally, the pass is minted fresh, never a stored copy served again. A phase=qualification completion does not carry one yet;
  • your storefront hands the patient over with a top-level form POST to https://{MEMBER_HOST}/api/checkout/door, with the pass in the capability field and your offering_ref beside it. The door checks the pass and sends the patient to checkout;
  • the POST must come from a page on one of your brand’s registered browser origins. Any refusal is the same plain not-found.
What you must do: keep the pass on your server, never in a URL or your page’s JavaScript. Replace your questions-first checkout link with the form POST, and register the origin your form page is served from, including any preview host. If the 10 minutes lapse, start the questions again: the pass is never reissued from the enrollment id. Checkout-first links are unchanged. See handing the patient to checkout.
APIAdditive
commit d92154c
Changed
APIAdditive
commit 3f8c391
Changed
APIBreaking
commit ccddb40
Changed
DocsBreaking
The reference now shows the headers you actually send
No endpoint changed — the reference did. Two header mistakes affected 18 operations, including both storefront reads:
  • X-Brand-Id was missing from the storefront and readiness reads, your pricing and starter merchandising writes, both order reads, and the qualification and question-library operations. Every call made with a key needs it, and a call without it is refused with 400. With a publishable key it must be the brand the key was issued for.
  • Headers you never send were listed as parameters, some marked required: X-Client-Id, X-Client-Brand-Ids, X-Client-Key-Class and x-service-authorization. The platform sets those itself. They are gone from the reference.
What you must do: send X-Brand-Id on every call made with a key. If you generated a client from the reference, regenerate it so it stops demanding the headers you never send. See Authentication.
DocsAdditive
Reference descriptions rewritten in plain terms
Several reference descriptions carried internal notes, section marks and pointers to fields that are not published. They now say only what you need. Two of them change what you should rely on:
  • Idempotency-Key on the brand, domain, offering and webhook writes: send a unique value on every write, but do not assume a repeated request with the same key is de-duplicated. The checkout-session create is unchanged.
  • blocked.code is an open set: treat a code you don’t recognise as a generic stop.
The reference page for setting your brand’s display identity has a shorter title, so its address changed to /api-reference/brand-configuration/set-your-brands-display-identity.
DocsBreaking
The quickstart's questionnaire loop is corrected
No endpoint changed — the quickstart did. As written it could not finish a questionnaire:
  • it started a session without offering, which a brand with more than one treatment refuses (422);
  • its first answer had the wrong shape and sent no node_id;
  • its loop waited for complete only, so it never stopped on blocked.
What you must do: start with ?offering= set to the product the patient chose. Send the served node_id with every answer, and shape the answer by the node’s control: codes for choices, value for text, email, number, date and phone. Keep submitting only while status is active, and stop on complete, blocked or abandoned. See the Quickstart and how serving works.
DocsBreaking
Onboarding progress is not an API call
No endpoint changed — what we told you about one did. The welcome kit named GET /v1/onboardings/{onboarding_id}/progress as a call you could make any time, and the reference listed it with no credential. Your API key cannot reach it: today it is served only to platform staff, and a call made with your API key is refused with 401. The reference now says so.What you must do: remove any call to it from your integration. To find out which onboarding step you are on and what is waiting on you, ask your onboarding contact and quote your onboarding id. See the Onboarding timeline.
APIAdditive
commit 0fe8cc3
Changed
DocsBreaking
Test patients carry TEST in their name
Test patients must carry the TEST marker. Use only invented test patients whose first or last name includes the uppercase word TEST. Payments are unchanged: test mode never charges a real card.What you must do: use only invented patients whose names carry the TEST marker, and never enter real patient data in this environment. See Environments.
DocsAdditive
The site kit repository is public
The fork-and-own storefront starter lives in the site kit repository, which is public. The guide previously said access was provided at onboarding; you can fork the latest tagged release directly. See The checkout door.
APIAdditive
commit 0935919
Changed
APIAdditive
commit f68751e
Changed
APIAdditive
commit fff6e10
Changed
APIAdditive
commit 8cbd74f
Changed
APIAdditive
commit 0af47c3
Added
DocsAdditive
A guide for connecting a Lovable-generated storefront
A new guide, Connect a Lovable storefront, covers wiring a storefront generated from a written prompt to the platform: which of your three credentials may ever reach browser code, the two browser-safe reads and the exact-origin allowlist that governs them, both ways into the flow, and a reusable prompt that holds no credential.It is explicit about what you can build against now and what depends on your brand’s configuration. The browser read needs two things switched on for your brand — a publishable key and a registered origin — and until both are in place every cross-origin call is refused, whatever credential it carries. The questions-first entry is a server-side path end to end: your page can never call those endpoints itself. And the guide repeats the rule that costs integrators the most: there is no redirect back to your storefront after payment, so a payment outcome is read with GET /v1/payments/{order_ref}/status or delivered by webhook — never inferred from the browser.One correction of terminology worth reading if you have built against another payment tool: on this platform a pk_ key is a server secret, and the browser-safe credential is the publishable brand key beginning pub_test_ or pub_live_.
DocsBreaking
The enrollment status read has documented outcomes other than a status
No endpoint changed — what we told you about one did. The guides said an enrollment id was readable for its status from the moment it existed. The contract declares more outcomes than that for GET /v1/journeys/{journey_id}/status: besides the 400/401/404 refusals, it can return 422 with the type https://purple.md/problems/journey-status-pending — a typed “no status yet” response for an enrollment that is yours — and 503 when the status source is unavailable. Code that treats any non-200 from that read as a failed call, or as a missing enrollment, needs a branch for it.The patient flow now documents those outcomes and how they differ: branch on the type, and read a 503 as “the read did not succeed” rather than as a statement about the patient. The quickstart, how it works, the checkout door, the identity handoff and identifiers and error types are corrected to match. The identity handoff also no longer implies that verification produces a status change of its own — it does not, and never did.
APIAdditive
commit ae1613d
Changed
APIAdditive
commit b459b2b
APIAdditive
commit 85c6665
Added
APIAdditive
commit a2a7fa8
Added
APIAdditive
commit 9016094
Changed
APIAdditive
commit aa30010
Changed
APIAdditive
commit 9f26db7
APIAdditive
commit f043001
Changed
APIBreaking
commit e8f344c
Changed
APIBreaking
commit 4c1ff5b
Changed
APIAdditive
commit ea56e47
Changed
DocsAdditive
Three new guides + guide fixes
Three new guides join the Integrate section: Pricing — set the price your patients pay, what the price floor means, and how the hosted checkout resolves your stored price; The patient flow — the enrollment stages end-to-end, the eleven-value public status vocabulary, and where the payment step sits; and Login URLs and domains — building the sign-in link, the member portal origin, and the self-serve custom-domain ladder. The welcome kit now lists your API base URL and member portal origin, and the base-URL wording across the guides is aligned: the test-environment base is issued with your key.
APIBreaking
commit 82f9057
AddedRemoved
  • GET /v1/journeys/{journey_id}/status
APIAdditive
commit 7662829
Added
APIAdditive
commit 0a3a895
Changed
DocsAdditive
How questions work
A new guide, How questions work, explains the model behind the question endpoints: facts and the shared catalog, controls, the line between your screening questions and the medical intake, and how the engine serves an intake one question at a time. It deep-links every question-authoring operation on the public reference.
APIAdditive
commit ba74887
Added
  • POST /v1/account/brands/{brand_id}/offerings — enableAccountOffering
  • PUT /v1/account/offerings/{ref}/merchandising — setAccountOfferingMerchandising
APIAdditive
commit 4b3fd94
Added
  • GET /v1/account/brands/{brand_id}/domains/{kind} — getAccountBrandDomain
  • GET /v1/account/brands/{brand_id}/domains — listAccountBrandDomains
  • DELETE /v1/account/brands/{brand_id}/domains/{kind} — removeAccountBrandDomain
  • PUT /v1/account/brands/{brand_id}/domains/{kind} — setAccountBrandDomain
  • POST /v1/account/brands/{brand_id}/domains/{kind}/verify — verifyAccountBrandDomain
APIAdditive
commit 970046f
Added
  • PUT /v1/account/brands/{brand_id}/config — setAccountBrandDisplay
Changed
  • GET /v1/instrument/resolve — resolveInstrument: parameter added: query:phase; response added: 400
APIAdditive
Brand configuration
The display identity (name, logo, theme tokens, support email) is now a self-serve write with your own API key at PUT /v1/account/brands/{brand_id}/config, with the matching readback on GET. The token reference and the copy wording wall are documented in the brand configuration guide.
DocsAdditive
docs-pipeline
This “API changes” page is now live, with automatic reference-change entries generated from the published contract on every deploy.