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

# Login URLs and domains

> How to build your patients' sign-in link, what the member portal origin is for, and how to put both on your own custom domains.

Two platform-hosted places carry your patients: the **sign-in page** (where a login begins) and
the **member portal** (where a patient who has logged in manages their plan, and where the hosted identity
step lives). Both work out of the box on shared platform domains — and both can wear a custom
domain of your own. This page covers the sign-in URL your storefront builds, what the member
origin is for, and the self-serve path to custom domains.

## The sign-in URL

Your storefront signs a patient in by sending their browser to `GET /v1/auth/login`, scoped to
your brand:

```
https://<your API base>/v1/auth/login?brand=brd_acme&returnTo=https://storefront.example.com/welcome-back
```

| Parameter    | Required | What it is                                                                                                                                       |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `brand`      | Yes      | Your brand id (`brd_...`). The login page wears this brand's look, and the session it starts is scoped to it.                                    |
| `returnTo`   | No       | Where to send the patient after sign-in. Honored **only** when it is on your brand's redirect allowlist — anything else is dropped, fail-closed. |
| `connection` | No       | A sign-in method hint (never a password method).                                                                                                 |

The endpoint answers with a redirect to the hosted Universal Login page — you can see it without
any credentials:

```bash theme={null}
curl --head "$BASE_URL/v1/auth/login?brand=brd_acme"
```

The response is a `302` whose `Location` is the hosted sign-in page. Send the patient's browser
through it and the platform does the rest: an emailed code, Google, or Apple — never a password —
then `GET /v1/auth/callback` completes the sign-in and starts the patient's session. The full
sequence is in the [identity handoff](/pages/integrate/the-identity-handoff) and
[authentication](/pages/authentication) guides.

## The member portal origin

The member portal is the platform-hosted origin where your logged-in patients land: their plan,
their orders, and the hosted [identity step](/pages/integrate/the-identity-handoff) at
`/identity` all live there. Your [welcome kit](/pages/integrate/welcome-kit) names your brand's
member origin — use it wherever your storefront links a patient "to their account", and as the
base for the identity handoff link.

Like sign-in, the member portal starts on a shared platform domain and can move to your own.

## Custom domains, self-serve

A custom domain is registered per **kind** — `login` for the sign-in page, `member` for the member
portal — with your own API key. A brand with no custom domain of a kind simply uses the shared
default; nothing in your flow is ever gated on registering one.

The ladder is the same for both kinds: **choose a hostname → add the CNAME → verify → certificate
→ active.**

### 1. Choose the hostname

```bash theme={null}
curl --request PUT "$BASE_URL/v1/account/brands/brd_acme/domains/login" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme" \
  --header "Idempotency-Key: domain-login-2026-08-24-a" \
  --header "Content-Type: application/json" \
  --data '{ "hostname": "login.example.com" }'
```

The row comes back with the **CNAME record to add to your DNS**:

```json theme={null}
{
  "data": {
    "brand_id": "brd_acme",
    "kind": "login",
    "hostname": "login.example.com",
    "state": "cname_rendered",
    "created_at": "2026-08-24T09:15:02Z",
    "updated_at": "2026-08-24T09:15:02Z",
    "cname": {
      "type": "CNAME",
      "name": "login.example.com",
      "value": "edge-target.purplelabelmd.com"
    }
  }
}
```

Re-sending the **same** hostname returns the current row unchanged; sending a **different**
hostname for a kind you already registered is a conflict — remove the old one first. Hostnames are
globally unique across the platform, so a hostname another brand holds is also a conflict.

### 2. Add the CNAME record

Create the record exactly as returned — `name` pointed at `value` — in your DNS provider. It is a
plain hostname pair, never a secret.

### 3. Verify and advance

```bash theme={null}
curl --request POST "$BASE_URL/v1/account/brands/brd_acme/domains/login/verify" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme"
```

Verification checks your CNAME and advances the row toward serving. If DNS has not propagated or
the certificate is not ready yet, the row stays where it is and you get a **retryable `409`** —
call it again after a while; the call is idempotent at or past each state it drives to.

The two kinds finish differently:

* A **login** domain proceeds through certificate issuance to `active` on its own — verify until
  the row reads `active`, and your patients sign in on your hostname.
* A **member** domain verifies the same way but stops at the certificate-request state; the final
  serving switch-on (`cert_issued` → `active`) is performed by the platform's operators once your
  certificate is issued. You will see the row advance to `active` without further calls from you.

Read any row back at
[`GET /v1/account/brands/{brand_id}/domains/{kind}`](/api-reference/brand-configuration/read-one-custom-domain-row),
or [list both](/api-reference/brand-configuration/list-your-brands-custom-domains) — each row
carries its current `state`, so polling the read is how you watch the ladder move:

```
requested → cname_rendered → verifying → verified → cert_issued → active        (login)
requested → cname_rendered → verifying → verified → cert_requested → cert_issued → active   (member)
```

### Removing a custom domain

[`DELETE /v1/account/brands/{brand_id}/domains/{kind}`](/api-reference/brand-configuration/remove-a-custom-domain-revert-to-the-shared-domain)
removes the row and reverts that kind to the shared-domain default.

All the domain endpoints are key-scoped and tenant-isolated the same way as the rest of your
account surface: the path `brand_id` must match your `X-Brand-Id`, and a foreign, unknown, or
mismatched brand answers `404`, never `403` — no cross-tenant existence leak.
