This platform issues two kinds of key, and they are not interchangeable. Which one an
operation accepts is decided by what the operation does, not by where you call it from.
API key
Publishable brand key
Prefix
pk_...
pub_test_... / pub_live_...
Scope
One client (tenant), all of its brands
Exactly one brand
May it appear in browser code?
No — server-side only
Yes — that is its purpose
Reaches
Every operation that declares it — which is every operation on this reference except the sign-in redirects and any that declares no credential at all
Two read-only operations, listed below
Issued
By your onboarding contact, shown once
By your onboarding contact, per brand, shown once
The prefix is not the convention you may expect. Here, pk_ marks the secret key. Some
payment tools use pk_ for their browser-safe key and sk_ for their secret — this platform does
not, and it issues no sk_ key at all. If a value beginning pk_ reaches browser code, a public
repository, or a prompt pasted into a code-generation tool, treat it as disclosed and rotate it.
Keys are client-scoped: one key resolves to exactly one client (tenant). Tenancy is never
read from request bodies.
Keys on this site are test-scoped — they can only reach the test-mode environment, where no
real card is ever charged. Use only invented test patients whose names include the uppercase word
TEST; see Environments.
Treat keys as secrets: server-side only, never in browser code or public repositories. Issue and
rotation are handled by your onboarding contact — there is no self-serve key generation. See
your credentials.
Revocation takes up to 30 seconds. Credential resolutions are cached for that long, and the
window is the upper bound on how long a revoked or rotated key can still be accepted. A key you
have just replaced is not dead the instant you replace it.
A read-only credential designed to live in a public page bundle. It is minted for one brand, and
no header can widen that. It reaches exactly two operations:
Two different refusals guard it, and they are not interchangeable:
Outside its capability — any other operation, and any write: 403, problem type
https://purple.md/problems/publishable-credential-read-only. The credential is real; what you
asked it to do is not something it may do.
Outside its brand — a brand that is not the one the key was minted for: 404, never
403, byte-identical to the answer for a brand that does not exist. A foreign brand, a brand
belonging to another client and an unknown brand are one answer, so nothing about another
tenant’s brands can be learned by asking.
Send it exactly as you send the API key, with the brand header alongside:
X-Brand-Id is required — omit it or malform it and the call is 400https://purple.md/problems/malformed-brand before anything is dispatched. With a publishable key
it must also equal the brand the key was minted for; any other value is the 404 above. Those
two headers are also exactly what the cross-origin policy permits, so nothing else you add will
survive a browser preflight.Three conditions before a browser read works at all:
Your brand has a publishable key issued.
You send X-Brand-Id and it matches that key’s brand.
The exact origin your page is served from is registered on your brand’s browser-origin
allowlist. Without that, the browser refuses the response regardless of the credential. See
replace the browser origins
and login URLs and domains.
Registering the origins is itself an API-key operation — it is account configuration, not a
browser call.Replacing a publishable key revokes the brand’s previous ones, and — like the API key — that lands
within one 30-second cache window, not instantly. A key you have pulled out of a page bundle can
still be accepted until the window closes, so rotating it starts the clock rather than stopping it.
Three operations are anonymous by design, because they are browser redirects that begin and end a
sign-in on the API host:
GET /v1/auth/login
GET /v1/auth/callback
GET /v1/auth/logout
They answer with a redirect rather than a 401, and there is nothing for your server to present.These are not how your patients sign in. Link patients to your member portal’s own sign-in route,
on the member portal address in your welcome kit:
https://{MEMBER_HOST}/auth/login?returnTo=/home
The patient must have a member portal session before they pay. The order belongs to the
account that is logged in to the portal at payment, and only that account’s portal shows it.
Do not use the API’s /v1/auth/login as your patients’ sign-in link.$PURPLE_API_BASE/v1/auth/login?brand=brd_... signs the patient in on the API host, with a
different sign-in application, and returns them there. It does not give the patient a member
portal session: a patient who signs in that way is still logged out of the portal, and has to sign
in again there before they can see their plan or orders. See
Login URLs and domains.
Every other operation on this reference is reached with a key.
Most endpoints also require the brand context header:
X-Brand-Id: brd_<your-brand>
The header is checked in two steps, and they answer differently:
Missing, or not a well-formed brd_ id — 400, type
https://purple.md/problems/malformed-brand. This is checked before your request is dispatched,
and it is a separate wall from the 401 above.
Well-formed, but not one of your brands — 404, never a 403. Existence is never revealed
across accounts, so a brand belonging to another client and a brand that does not exist give the
same answer.
Every operation that takes a key requires X-Brand-Id, and every one of them lists it in its
reference entry.
Authentication failures return 401 with an RFC 7807application/problem+json body. The body is deliberately opaque — it never reveals whether a
key exists, whether it was revoked, or whether the path you called is real.
The interactive playground reads its auth schemes from the API specification. Enter your test
key in the playground’s auth field once; it rides every try-it request from your browser to the
test environment.The two brand reads above declare both schemes, so you can drive them with either credential. What
the playground cannot reproduce is the browser-origin allowlist — it is not your page’s origin —
so a publishable key that works there still needs your origin registered before it works from your
own site.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Purple Label MD","description":"Public API reference for building a patient storefront on the platform. Generated from the platform's own API contracts, and matches the newest staging release.","colors":{"primary":"#6D28D9","light":"#A78BFA","dark":"#5B21B6"},"logo":{"light":"https://mintcdn.com/purplelabelmd/1P9p4n7lsNL4z3Jc/logo/light.svg?fit=max\u0026auto=format\u0026n=1P9p4n7lsNL4z3Jc\u0026q=85\u0026s=3c0569e3f76e49e04d232187908c2e2e","dark":"https://mintcdn.com/purplelabelmd/1P9p4n7lsNL4z3Jc/logo/dark.svg?fit=max\u0026auto=format\u0026n=1P9p4n7lsNL4z3Jc\u0026q=85\u0026s=c11f42bb461c28528be896c7c1a80efa"},"favicon":"https://mintcdn.com/purplelabelmd/MUdvgTlYoCGY54e8/favicon.svg?fit=max\u0026auto=format\u0026n=MUdvgTlYoCGY54e8\u0026q=85\u0026s=3bd633ab20ff170e0ebebec0406e6972","footer":{"socials":{}},"fonts":{"heading":{"family":"Geist"},"body":{"family":"Geist"}},"styling":{"latex":false},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"purplelabelmd","hasAudioNarration":false,"actualSubdomain":"purplelabelmd","customDomains":["docs.purplelabelmd.com"],"gitSource":{"type":"github","owner":"purple-label-md","repo":"purple-doc","deployBranch":"main","contentDirectory":"","isPrivate":true,"mountPath":"","label":""},"entitlements":{"AI_CHAT":{"status":"ENABLED"},"WEB_MCP":{"status":"ENABLED"},"NARRATION":{"status":"ENABLED"}},"buildId":"6af545312fc9183f3253a260","clientVersion":"0.0.4111","searchFilterCounts":[{"language":"default","tag":"API reference","count":50},{"language":"default","tag":"Guides","count":21},{"language":"default","tag":"Offering configuration","count":18},{"language":"default","tag":"Integrate","count":16},{"language":"default","tag":"Brand configuration","count":9},{"language":"default","tag":"Webhooks","count":7},{"language":"default","tag":"Payments","count":4},{"language":"default","tag":"Intake","count":3},{"language":"default","tag":"Onboarding","count":3},{"language":"default","tag":"Sessions \u0026 authentication","count":3}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":true,"hasWebMcpPermissions":true,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-rbac-roles","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-metadata","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":true},"customCss":[{"filePath":"style.css","content":"@media (prefers-reduced-motion: no-preference){.plmd-logo{transform-origin:center;transition:transform 0.12s ease}.plmd-logo .plmd-mark path{transform-box:fill-box;transform-origin:center}.plmd-logo-link:active .plmd-logo{transform:scale(0.85)}.plmd-logo-link:hover .plmd-logo .plmd-mark path:last-child{animation:plmd-carrot-hop 0.55s ease}.plmd-logo-link:hover .plmd-logo .plmd-mark path:nth-child(-n+6){animation:plmd-carrot-twinkle 0.5s ease}.plmd-logo-link:hover .plmd-logo .plmd-mark path:nth-child(even){animation-delay:0.08s}}@keyframes plmd-carrot-hop{0%{transform:translateY(0) rotate(0)}30%{transform:translateY(-14%) rotate(-9deg)}55%{transform:translateY(0) rotate(6deg)}78%{transform:translateY(-5%) rotate(-2deg)}100%{transform:translateY(0) rotate(0)}}@keyframes plmd-carrot-twinkle{0%,100%{transform:scale(1);opacity:1}45%{transform:scale(1.6);opacity:0.7}}"}],"page":{"pagePath":"pages/authentication","slug":"/pages/authentication","pageMetadata":{"title":"Authentication","description":"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.","href":"/pages/authentication"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,li:`li`,ol:`ol`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Table,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsx(Heading,{level:`2`,id:`two-credential-classes`,children:`Two credential classes`}),`\n`,_jsxs(_components.p,{children:[`This platform issues `,_jsx(_components.strong,{children:`two`}),` kinds of key, and they are not interchangeable. Which one an\noperation accepts is decided by what the operation does, not by where you call it from.`]}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{}),_jsx(_components.th,{children:`API key`}),_jsx(_components.th,{children:`Publishable brand key`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Prefix`}),_jsx(_components.td,{children:_jsx(_components.code,{children:`pk_...`})}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`pub_test_...`}),` / `,_jsx(_components.code,{children:`pub_live_...`})]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Scope`}),_jsx(_components.td,{children:`One client (tenant), all of its brands`}),_jsx(_components.td,{children:`Exactly one brand`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`May it appear in browser code?`}),_jsx(_components.td,{children:_jsx(_components.strong,{children:`No — server-side only`})}),_jsx(_components.td,{children:_jsx(_components.strong,{children:`Yes — that is its purpose`})})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Reaches`}),_jsx(_components.td,{children:`Every operation that declares it — which is every operation on this reference except the sign-in redirects and any that declares no credential at all`}),_jsx(_components.td,{children:`Two read-only operations, listed below`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:`Issued`}),_jsx(_components.td,{children:`By your onboarding contact, shown once`}),_jsx(_components.td,{children:`By your onboarding contact, per brand, shown once`})]})]})]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The prefix is not the convention you may expect.`}),` Here, `,_jsx(_components.code,{children:`pk_`}),` marks the `,_jsx(_components.strong,{children:`secret`}),` key. Some\npayment tools use `,_jsx(_components.code,{children:`pk_`}),` for their browser-safe key and `,_jsx(_components.code,{children:`sk_`}),` for their secret — this platform does\nnot, and it issues no `,_jsx(_components.code,{children:`sk_`}),` key at all. If a value beginning `,_jsx(_components.code,{children:`pk_`}),` reaches browser code, a public\nrepository, or a prompt pasted into a code-generation tool, treat it as disclosed and rotate it.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`the-api-key`,children:`The API key`}),`\n`,_jsx(_components.p,{children:`Presented as a bearer token:`}),`\n`,_jsx(CodeBlock,{numberOfLines:`2`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`2`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`Authorization: Bearer \u003cyour-test-api-key>`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`Keys are `,_jsx(_components.strong,{children:`client-scoped`}),`: one key resolves to exactly one client (tenant). Tenancy is never\nread from request bodies.`]}),`\n`,_jsxs(_components.li,{children:[`Keys on this site are `,_jsx(_components.strong,{children:`test-scoped`}),` — they can only reach the test-mode environment, where no\nreal card is ever charged. Use only invented test patients whose names include the uppercase word\n`,_jsx(_components.code,{children:`TEST`}),`; see `,_jsx(_components.a,{href:`/pages/environments`,children:`Environments`}),`.`]}),`\n`,_jsxs(_components.li,{children:[`Treat keys as secrets: server-side only, never in browser code or public repositories. Issue and\nrotation are handled by your onboarding contact — there is no self-serve key generation. See\n`,_jsx(_components.a,{href:`/pages/integrate/your-credentials`,children:`your credentials`}),`.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Revocation takes up to 30 seconds.`}),` Credential resolutions are cached for that long, and the\nwindow is the upper bound on how long a revoked or rotated key can still be accepted. A key you\nhave just replaced is not dead the instant you replace it.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`the-publishable-brand-key`,children:`The publishable brand key`}),`\n`,_jsxs(_components.p,{children:[`A read-only credential designed to live in a public page bundle. It is minted for `,_jsx(_components.strong,{children:`one brand`}),`, and\nno header can widen that. It reaches exactly two operations:`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:_jsx(_components.a,{href:`/api-reference/onboarding/read-one-offerings-price-and-whether-your-brand-can-sell-it`,children:_jsx(_components.code,{children:`GET /v1/account/brands/{brand_id}/storefront`})})}),`\n`,_jsx(_components.li,{children:_jsx(_components.a,{href:`/api-reference/onboarding/check-whether-your-brand-is-ready-to-sell`,children:_jsx(_components.code,{children:`GET /v1/account/brands/{brand_id}/readiness`})})}),`\n`]}),`\n`,_jsx(_components.p,{children:`Two different refusals guard it, and they are not interchangeable:`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Outside its capability`}),` — any other operation, and any write: `,_jsx(_components.code,{children:`403`}),`, problem type\n`,_jsx(_components.code,{children:`https://purple.md/problems/publishable-credential-read-only`}),`. The credential is real; what you\nasked it to do is not something it may do.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Outside its brand`}),` — a brand that is not the one the key was minted for: `,_jsx(_components.code,{children:`404`}),`, `,_jsxs(_components.strong,{children:[`never\n`,_jsx(_components.code,{children:`403`})]}),`, byte-identical to the answer for a brand that does not exist. A foreign brand, a brand\nbelonging to another client and an unknown brand are one answer, so nothing about another\ntenant’s brands can be learned by asking.`]}),`\n`]}),`\n`,_jsx(_components.p,{children:`Send it exactly as you send the API key, with the brand header alongside:`}),`\n`,_jsx(CodeBlock,{numberOfLines:`3`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`3`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`Authorization: Bearer pub_test_\u003cyour-brand-key>`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`X-Brand-Id: brd_\u003cyour-brand>`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`X-Brand-Id`}),` is `,_jsx(_components.strong,{children:`required`}),` — omit it or malform it and the call is `,_jsx(_components.code,{children:`400`}),`\n`,_jsx(_components.code,{children:`https://purple.md/problems/malformed-brand`}),` before anything is dispatched. With a publishable key\nit must also `,_jsx(_components.strong,{children:`equal the brand the key was minted for`}),`; any other value is the `,_jsx(_components.code,{children:`404`}),` above. Those\ntwo headers are also exactly what the cross-origin policy permits, so nothing else you add will\nsurvive a browser preflight.`]}),`\n`,_jsx(_components.p,{children:`Three conditions before a browser read works at all:`}),`\n`,_jsxs(_components.ol,{children:[`\n`,_jsx(_components.li,{children:`Your brand has a publishable key issued.`}),`\n`,_jsxs(_components.li,{children:[`You send `,_jsx(_components.code,{children:`X-Brand-Id`}),` and it matches that key’s brand.`]}),`\n`,_jsxs(_components.li,{children:[`The exact origin your page is served from is registered on your brand’s browser-origin\nallowlist. Without that, the browser refuses the response regardless of the credential. See\n`,_jsx(_components.a,{href:`/api-reference/brand-configuration/replace-the-browser-origins-your-storefront-may-read-this-brand-from`,children:`replace the browser origins`}),`\nand `,_jsx(_components.a,{href:`/pages/integrate/login-urls-and-domains`,children:`login URLs and domains`}),`.`]}),`\n`]}),`\n`,_jsx(_components.p,{children:`Registering the origins is itself an API-key operation — it is account configuration, not a\nbrowser call.`}),`\n`,_jsxs(_components.p,{children:[`Replacing a publishable key revokes the brand’s previous ones, and — like the API key — that lands\n`,_jsx(_components.strong,{children:`within one 30-second cache window`}),`, not instantly. A key you have pulled out of a page bundle can\nstill be accepted until the window closes, so rotating it starts the clock rather than stopping it.`]}),`\n`,_jsx(Heading,{level:`2`,id:`what-needs-no-key`,children:`What needs no key`}),`\n`,_jsx(_components.p,{children:`Three operations are anonymous by design, because they are browser redirects that begin and end a\nsign-in on the API host:`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:_jsx(_components.code,{children:`GET /v1/auth/login`})}),`\n`,_jsx(_components.li,{children:_jsx(_components.code,{children:`GET /v1/auth/callback`})}),`\n`,_jsx(_components.li,{children:_jsx(_components.code,{children:`GET /v1/auth/logout`})}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`They answer with a redirect rather than a `,_jsx(_components.code,{children:`401`}),`, and there is nothing for your server to present.`]}),`\n`,_jsxs(_components.p,{children:[`These are not how your patients sign in. Link patients to your member portal’s own sign-in route,\non the member portal address in your `,_jsx(_components.a,{href:`/pages/integrate/welcome-kit`,children:`welcome kit`}),`:`]}),`\n`,_jsx(CodeBlock,{numberOfLines:`2`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`2`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`https://{MEMBER_HOST}/auth/login?returnTo=/home`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The patient must have a member portal session before they pay.`}),` The order belongs to the\naccount that is logged in to the portal at payment, and only that account’s portal shows it.`]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[_jsxs(_components.strong,{children:[`Do not use the API’s `,_jsx(_components.code,{children:`/v1/auth/login`}),` as your patients’ sign-in link.`]}),`\n`,_jsx(_components.code,{children:`$PURPLE_API_BASE/v1/auth/login?brand=brd_...`}),` signs the patient in on the API host, with a\ndifferent sign-in application, and returns them there. It does `,_jsx(_components.strong,{children:`not`}),` give the patient a member\nportal session: a patient who signs in that way is still logged out of the portal, and has to sign\nin again there before they can see their plan or orders. See\n`,_jsx(_components.a,{href:`/pages/integrate/login-urls-and-domains`,children:`Login URLs and domains`}),`.`]})}),`\n`,_jsx(_components.p,{children:`Every other operation on this reference is reached with a key.`}),`\n`,_jsx(Heading,{level:`2`,id:`brand-scoping`,children:`Brand scoping`}),`\n`,_jsx(_components.p,{children:`Most endpoints also require the brand context header:`}),`\n`,_jsx(CodeBlock,{numberOfLines:`2`,language:`text`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`text`,children:_jsxs(_components.code,{language:`text`,numberOfLines:`2`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{children:`X-Brand-Id: brd_\u003cyour-brand>`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{})})]})})}),`\n`,_jsx(_components.p,{children:`The header is checked in two steps, and they answer differently:`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`Missing, or not a well-formed `,_jsx(_components.code,{children:`brd_`}),` id`]}),` — `,_jsx(_components.code,{children:`400`}),`, type\n`,_jsx(_components.code,{children:`https://purple.md/problems/malformed-brand`}),`. This is checked before your request is dispatched,\nand it is a separate wall from the `,_jsx(_components.code,{children:`401`}),` above.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Well-formed, but not one of your brands`}),` — `,_jsx(_components.code,{children:`404`}),`, never a `,_jsx(_components.code,{children:`403`}),`. Existence is never revealed\nacross accounts, so a brand belonging to another client and a brand that does not exist give the\nsame answer.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`Every operation that takes a key requires `,_jsx(_components.code,{children:`X-Brand-Id`}),`, and every one of them lists it in its\nreference entry.`]}),`\n`,_jsx(Heading,{level:`2`,id:`errors`,children:`Errors`}),`\n`,_jsxs(_components.p,{children:[`Authentication failures return `,_jsx(_components.code,{children:`401`}),` with an `,_jsx(_components.a,{href:`https://www.rfc-editor.org/rfc/rfc7807`,children:`RFC 7807`}),`\n`,_jsx(_components.code,{children:`application/problem+json`}),` body. The body is deliberately opaque — it never reveals whether a\nkey exists, whether it was revoked, or whether the path you called is real.`]}),`\n`,_jsx(Heading,{level:`2`,id:`playground-authentication`,children:`Playground authentication`}),`\n`,_jsx(_components.p,{children:`The interactive playground reads its auth schemes from the API specification. Enter your test\nkey in the playground’s auth field once; it rides every try-it request from your browser to the\ntest environment.`}),`\n`,_jsx(_components.p,{children:`The two brand reads above declare both schemes, so you can drive them with either credential. What\nthe playground cannot reproduce is the browser-origin allowlist — it is not your page’s origin —\nso a publishable key that works there still needs your origin registered before it works from your\nown site.`})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Authentication","description":"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.","href":"/pages/authentication"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"Two credential classes","slug":"two-credential-classes","depth":2,"children":[]},{"title":"The API key","slug":"the-api-key","depth":2,"children":[]},{"title":"The publishable brand key","slug":"the-publishable-brand-key","depth":2,"children":[]},{"title":"What needs no key","slug":"what-needs-no-key","depth":2,"children":[]},{"title":"Brand scoping","slug":"brand-scoping","depth":2,"children":[]},{"title":"Errors","slug":"errors","depth":2,"children":[]},{"title":"Playground authentication","slug":"playground-authentication","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":["/_mintlify/custom/purplelabelmd/3b4d4a495cd0c48d/brand.js"],"subdomain":"purplelabelmd","siteName":"Purple Label MD","title":"Authentication - Purple Label MD","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-11-12T02:36:05.987Z","originalFileLocation":"pages/authentication.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Purple Label MD\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Authentication - Purple Label MD\">\u003cmeta data-mint-head property=\"og:description\" content=\"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Purple Label MD\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.purplelabelmd.com/pages/authentication\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://purplelabelmd.mintlify.site/_mintlify/api/og/v2?division=Documentation\u0026amp;title=Authentication\u0026amp;description=Two+credential+classes%2C+which+operations+each+one+reaches%2C+brand+scoping%2C+and+how+the+playground+authenticates.\u0026amp;theme=1cfc55286f6f85a93657a32a\u0026amp;signature=5cbe33b95ea2659955bb5d11d44146a4a9a80055cf88d3fc8dff50b30f617cfd\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Authentication - Purple Label MD\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://purplelabelmd.mintlify.site/_mintlify/api/og/v2?division=Documentation\u0026amp;title=Authentication\u0026amp;description=Two+credential+classes%2C+which+operations+each+one+reaches%2C+brand+scoping%2C+and+how+the+playground+authenticates.\u0026amp;theme=1cfc55286f6f85a93657a32a\u0026amp;signature=5cbe33b95ea2659955bb5d11d44146a4a9a80055cf88d3fc8dff50b30f617cfd\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/purplelabelmd/TU4Si8SnCOFGjzcC/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Purple Label MD\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#6D28D9\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.purplelabelmd.com/pages/authentication\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/pages/authentication.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.purplelabelmd.com/#organization\",\"name\":\"Purple Label MD\",\"url\":\"https://docs.purplelabelmd.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/purplelabelmd/1P9p4n7lsNL4z3Jc/logo/light.svg?fit=max\u0026auto=format\u0026n=1P9p4n7lsNL4z3Jc\u0026q=85\u0026s=3c0569e3f76e49e04d232187908c2e2e\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.purplelabelmd.com#website\",\"name\":\"Purple Label MD\",\"url\":\"https://docs.purplelabelmd.com\",\"publisher\":{\"@id\":\"https://docs.purplelabelmd.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.purplelabelmd.com/pages/authentication#webpage\",\"url\":\"https://docs.purplelabelmd.com/pages/authentication\",\"name\":\"Authentication\",\"description\":\"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.\",\"dateModified\":\"2026-11-12T02:36:05.987Z\",\"isPartOf\":{\"@id\":\"https://docs.purplelabelmd.com#website\"}},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.purplelabelmd.com/pages/authentication#article\",\"headline\":\"Authentication\",\"name\":\"Authentication\",\"description\":\"Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.\",\"url\":\"https://docs.purplelabelmd.com/pages/authentication\",\"mainEntityOfPage\":{\"@id\":\"https://docs.purplelabelmd.com/pages/authentication#webpage\"},\"dateModified\":\"2026-11-12T02:36:05.987Z\",\"publisher\":{\"@id\":\"https://docs.purplelabelmd.com/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.purplelabelmd.com#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Purple Label MD\",\"publisher\":{\"@type\":\"Organization\",\"name\":\"Purple Label MD\"},\"provider\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","agentPointerHref":"/pages/authentication.md","siteName":"Purple Label MD","nav":{"global":null,"tabs":[{"tab":"Guides","pages":[{"title":"Overview","href":"/pages/overview"},{"title":"CHANGELOG","href":"/pages/api-changes"},{"title":"Quickstart","description":"Drive a synthetic journey end-to-end from this site in a few minutes - test mode only.","href":"/pages/quickstart"},{"title":"Authentication","href":"/pages/authentication"},{"title":"Environments","description":"The test environment is the only environment this site talks to: its hosts, its sign-in page, and what makes it a test.","href":"/pages/environments"},{"group":"Integrate","pages":[{"title":"How it works","href":"/pages/integrate/how-it-works"},{"title":"Walkthrough","href":"/pages/integrate/walkthrough"},{"title":"The patient flow","href":"/pages/integrate/the-patient-flow"},{"title":"How intake serving works","href":"/pages/integrate/how-serving-works"},{"title":"How questions work","href":"/pages/integrate/how-questions-work"},{"title":"The checkout door","href":"/pages/integrate/the-checkout-door"},{"title":"Pricing","href":"/pages/integrate/pricing"},{"title":"Identifiers and error types","href":"/pages/integrate/identifiers-and-error-types"},{"title":"The identity handoff","href":"/pages/integrate/the-identity-handoff"},{"title":"Brand configuration","href":"/pages/integrate/brand-configuration"},{"title":"List your offerings","href":"/pages/integrate/your-offerings"},{"title":"Login URLs and domains","href":"/pages/integrate/login-urls-and-domains"},{"title":"Welcome kit","href":"/pages/integrate/welcome-kit"},{"title":"Connect a Lovable storefront","href":"/pages/integrate/lovable-storefront"},{"title":"Your credentials","href":"/pages/integrate/your-credentials"},{"title":"Onboarding timeline","href":"/pages/integrate/onboarding-timeline"}]}]},{"tab":"API reference","pages":[{"title":"API reference","href":"/api-reference/brand-configuration/read-back-your-brands-platform-configuration"}]}]},"basePath":"","title":"Authentication - Purple Label MD","buildId":"dpl_DqYu5MPb49gWBBbUGgpHQUmCHY3P","deploymentId":"dpl_DqYu5MPb49gWBBbUGgpHQUmCHY3P","dynamics":[],"chunks":[]}