Two credential classes
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.
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.
The API key
Presented as a bearer token:
- 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 and can
never touch live rails.
- 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.
The publishable brand key
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 400
https://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.
What needs no key
Three operations are anonymous by design, because they are browser redirects that begin and end a
patient’s sign-in:
GET /v1/auth/login
GET /v1/auth/callback
GET /v1/auth/logout
Send the patient’s browser through them. They answer with a redirect rather than a 401, and there
is nothing for your server to present.
Every other operation on this reference is reached with a key.
Brand scoping
Most endpoints also require the brand context header:
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.
Some operations additionally show X-Client-Id and X-Client-Brand-Ids in their reference entry.
Those are resolved from your key and attached for you; you do not send them.
Errors
Authentication failures return 401 with an RFC 7807
application/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.
Playground authentication
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.