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

# Patient portal & messaging

> OTP login, case timeline, secure patient↔care-team messaging, and photo-ID upload — the patient-facing half of Tagada Rx.

# Patient portal & messaging

After purchase, patients need three things: **see where their treatment stands**, **talk to their care team**, and (for some offerings) **verify their identity**. Tagada Rx ships all three as portal endpoints your storefront can skin however it likes — the Oakwell template's `/portal` page is a full reference implementation.

<Info>
  **Auth model:** portal routes are scoped to the **customer**, not the merchant. The patient logs in with an **email OTP** which creates a CMS session; every portal call carries that session token (`x-cms-token`). With the Plugin SDK this is automatic once the customer has logged in via `useLogin` — no extra wiring.
</Info>

***

## The endpoints

All under `/api/v1/rx/portal/*`, CMS-session authenticated:

| Endpoint                                  | What it returns                                                        |
| ----------------------------------------- | ---------------------------------------------------------------------- |
| `GET /rx/portal/cases`                    | The customer's cases (status, product, amounts, `canMessage`)          |
| `GET /rx/portal/cases/{caseId}`           | One case + its lifecycle timeline (`events`)                           |
| `GET /rx/portal/cases/{caseId}/messages`  | The patient ↔ care-team thread, relayed live from the clinical network |
| `POST /rx/portal/cases/{caseId}/messages` | Send a patient message (text + optional attachments)                   |
| `POST /rx/portal/cases/{caseId}/identity` | Upload a photo ID for identity verification                            |

Access control is strict: a case is only visible if it belongs to the authenticated customer, and messaging requires the case to have a linked network patient (`canMessage: true`).

***

## Messaging

The thread is **relayed** from the clinical network — Tagada stores no message bodies (PHI transit rule). Messages carry an author so you can style the bubbles:

```json theme={null}
{
  "messages": [
    {
      "externalMessageId": "msg_…",
      "channel": "patient",
      "author": "clinician",
      "authorName": "Dr. Smith",
      "text": "Hi Jane — your dosage looks right. Any side effects?",
      "files": [],
      "createdAt": "2026-07-16T22:30:00Z"
    }
  ]
}
```

Sending, with optional attachments (images, documents, audio, video — base64-encoded):

```json theme={null}
POST /api/v1/rx/portal/cases/rxcase_xxx/messages
{
  "text": "Here's the photo you asked for.",
  "attachments": [
    { "filename": "arm.jpg", "mimeType": "image/jpeg", "contentBase64": "…" }
  ]
}
```

### Notifications

When the **care team** writes (clinician or support — patient-authored messages are filtered out), Tagada emails the patient a content-free notification — *"You have a new message from your care team"* — deep-linking to your portal page. The email never contains the message text.

<Note>
  This is why your storefront should have a stable portal URL: the notification links there. The Oakwell template registers `/portal` and the thank-you page links to it.
</Note>

***

## Identity verification

Offerings mapped with `requiresIdVerification: true` need a government photo ID before the clinician can prescribe. One call handles upload + attachment to the network patient:

```json theme={null}
POST /api/v1/rx/portal/cases/rxcase_xxx/identity
{
  "filename": "license.jpg",
  "mimeType": "image/jpeg",
  "contentBase64": "…"
}
```

Show the upload block whenever the case's offering requires it and the case is still pre-approval — the Oakwell portal renders it inline in the case detail view.

***

## Plugin SDK wiring (Oakwell pattern)

```tsx theme={null}
import { useTagada, useLogin } from '@tagadapay/plugin-sdk/react';
import { createPortalClient, fileToAttachment } from '@/lib/portal-client';

function Portal() {
  const { apiService } = useTagada();
  const { isAuthenticated, sendOtp, verifyOtp } = useLogin();
  const portal = useMemo(() => createPortalClient(apiService), [apiService]);

  // 1. OTP gate
  if (!isAuthenticated) return <OtpLogin onSend={sendOtp} onVerify={verifyOtp} />;

  // 2. Then everything just works:
  //    portal.listCases()
  //    portal.getCase(caseId)          → { case, events } timeline
  //    portal.listMessages(caseId)
  //    portal.sendMessage(caseId, { text, attachments })
  //    portal.uploadIdentity(caseId, await fileToAttachment(file))
}
```

From a **headless** site, call the same endpoints with the CMS session token you obtain from the customer OTP login flow (`tagada.customer` module), passed as the `x-cms-token` header.

***

## UX checklist for a good portal

* **Timeline, not jargon** — render `case.in_review` as "A clinician is reviewing your information", with dates.
* **Message composer with attachments** — patients often need to send photos; accept camera capture on mobile.
* **ID upload prompt** — surface it prominently while the case waits on identity verification; it's the #1 stall cause.
* **Content-free everywhere** — never mirror message text into your own emails, analytics, or logs.
