> ## Documentation Index
> Fetch the complete documentation index at: https://docs.purplelabelmd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Two credential classes, which operations each one reaches, brand scoping, and how the playground authenticates.

## 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.

|                                | 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 |

<Warning>
  **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.
</Warning>

## The API key

Presented as a bearer token:

```
Authorization: Bearer <your-test-api-key>
```

* 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](/pages/integrate/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:

* [`GET /v1/account/brands/{brand_id}/storefront`](/api-reference/onboarding/read-one-offerings-price-and-whether-your-brand-can-sell-it)
* [`GET /v1/account/brands/{brand_id}/readiness`](/api-reference/onboarding/check-whether-your-brand-is-ready-to-sell)

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:

```
Authorization: Bearer pub_test_<your-brand-key>
X-Brand-Id: brd_<your-brand>
```

`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:

1. Your brand has a publishable key issued.
2. You send `X-Brand-Id` and it matches that key's brand.
3. 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](/api-reference/brand-configuration/replace-the-browser-origins-your-storefront-may-read-this-brand-from)
   and [login URLs and domains](/pages/integrate/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:

```
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.

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](https://www.rfc-editor.org/rfc/rfc7807)
`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.
