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

# Walkthrough

> Integrate end to end: authenticate, run the questionnaire, and receive events.

This is the end-to-end developer path. Every example runs against the **test-mode environment** with
**test-scoped keys** and **synthetic data only**. The request and response shapes shown here are the
same ones generated into the [API reference](/pages/overview) — they cannot drift from the live API.

## Authenticate your requests

Server-to-server endpoints take a per-client API key as a bearer token and the brand context header:

```
Authorization: Bearer <your-test-api-key>
X-Brand-Id: brd_acme
```

The key resolves to exactly one client (your tenant); the brand must belong to that client. A
foreign or unknown brand returns `404`, and an authentication failure returns `401` with an opaque
[RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) body. Patient sign-in is separate — send patients
to `GET /v1/auth/login`, scoped with the `brand` parameter, and complete it at the callback.

## Start a patient with entry context

The entry-context parameters ride `GET /v1/instrument/resolve`. Every one of them is optional in the
contract, and each one fails closed — but `offering` becomes **required in practice** for a brand
with more than one enabled treatment, because without it the platform cannot tell which
questionnaire to serve and will not guess:

| Parameter       | What it does                                                                                      | Fail-closed rule                                                                                                                                                                                                                                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `offering`      | Preselect a catalog offering by its **stable reference**                                          | A free-text product name is refused. A reference the catalog cannot bind at all selects nothing and the shared questionnaire serves as before. A reference for a **different treatment** than the session resolved is refused with `422` — never served under the wrong questions. For a brand with more than one enabled treatment, omitting it is also `422`. |
| `phase`         | Serve one tier of the questionnaire on its own — `qualification` before payment, `clinical` after | Requires a questionnaire configured in tiers; an unknown value is rejected with `400`. Omitted means the whole questionnaire in one pass.                                                                                                                                                                                                                       |
| `redirect`      | Post-completion return URL                                                                        | Honored only if it matches your brand's redirect allowlist; otherwise dropped                                                                                                                                                                                                                                                                                   |
| `promo`         | Opaque promo token, passed through unchanged                                                      | Never interpreted here — validated downstream                                                                                                                                                                                                                                                                                                                   |
| `prefill_email` | Stage a contact-email prefill                                                                     | Lands **unverified**; nothing is written until the patient confirms it                                                                                                                                                                                                                                                                                          |
| `test`          | Per-client test-mode flag                                                                         | Passed through unchanged; a test session is synthetic end to end                                                                                                                                                                                                                                                                                                |

## Resolve the first question

```bash theme={null}
curl "$BASE_URL/v1/instrument/resolve?offering=SYN-TRZ-3M&test=true" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme"
```

`offering` takes a **stable offering reference** (the catalog code, for example `SYN-TRZ-3M`), never
a free-text product name — an unresolvable value is dropped and the generic flow runs. The response
is the current step. On the first call the server mints an enrollment id, returned as `journey_id`
(prefixed `jny_`); send it back in the `X-Journey-Id` header on a return visit to resume the same
session.

```json theme={null}
{
  "session_id": "sess_a1b2c3",
  "journey_id": "jny_8kQ2mX",
  "status": "active",
  "node": {
    "node_id": "contact_email",
    "kind": "question",
    "control": "email",
    "copy": "What email should we use to reach you?",
    "required": true
  },
  "flags": []
}
```

## Submit an answer, get the next question

Send the answer for the question the session is on. The answer is re-validated on the server, and
the session advances exactly one step.

```bash theme={null}
curl --request POST "$BASE_URL/v1/instrument/next" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme" \
  --header "Content-Type: application/json" \
  --data '{"session_id": "sess_a1b2c3", "answer": {"value": "jane@example.com"}}'
```

A valid answer returns the next step. If the answer fails validation the response is `422` and the
same question is re-presented with the problems in `issues`:

```json theme={null}
{
  "session_id": "sess_a1b2c3",
  "journey_id": "jny_8kQ2mX",
  "status": "active",
  "node": { "node_id": "contact_email", "kind": "question", "control": "email", "required": true },
  "issues": [
    { "code": "invalid_email", "message": "Enter a valid email address." }
  ]
}
```

Repeat until `status` is `complete`. The client never decides completion — the server does, once
every required answer is in.

## Register a webhook and verify its signature

Register the endpoint that should receive events. The registration is scoped to your client and
brand automatically — no request field chooses them.

```bash theme={null}
curl --request POST "$BASE_URL/v1/webhooks/registrations" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://storefront.example.com/hooks/purple",
    "event_types": ["journey.intake.submitted.v1", "journey.visit.completed.v1"]
  }'
```

The `201` response is the only message that ever carries the secret. Store `secret` immediately — it
is never returned again.

```json theme={null}
{
  "data": {
    "registration_id": "whr_2Ab9",
    "brand_id": "brd_acme",
    "url": "https://storefront.example.com/hooks/purple",
    "event_types": ["journey.intake.submitted.v1", "journey.visit.completed.v1"],
    "enabled": true,
    "secret_set": true,
    "created_at": "2026-02-01T12:00:00Z"
  },
  "secret": "psig_9f8e7d6c5b4a3210"
}
```

Every delivery is authenticated with an HMAC computed from that secret. To verify a delivery,
recompute the HMAC over the exact raw request body with your stored secret and compare it to the
signature on the delivery in constant time; reject any delivery that does not match. Because the
secret is shown only once, keep it somewhere your receiver can read at verification time.

`GET /v1/webhooks/event-types` returns the catalog you can subscribe to, including which correlation
id each event family carries.

## Fire a test event

Before you go live, send a synthetic event through the same delivery and signing path:

```bash theme={null}
curl --request POST "$BASE_URL/v1/webhooks/registrations/whr_2Ab9/test" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme" \
  --header "Content-Type: application/json" \
  --data '{"event_type": "journey.intake.submitted.v1"}'
```

The event is clearly marked as a test and carries synthetic identifiers. It is a single attempt with
no retries, and the outcome is returned in the response — the fastest way to prove your receiver and
your signature check work.

## Test-mode semantics

A session started with `test=true`, and every event fired from a test call, is synthetic from end to
end. Test enrollments are structurally excluded from settlement, reporting, and clinician-facing
queues; a test session can never charge a real card or ship a real order. Production access is a
separate, later step your onboarding contact owns — see [Environments](/pages/environments).
