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

# How it works

> Architecture, PHI handling, clinical webhooks, and the ship-day rebill engine — the Tagada Rx mental model.

# How Tagada Rx works

One page, four concepts: the **clinical network adapter**, the **PHI transit rule**, the **webhook pipeline**, and the **ship-day rebill engine**. Understand these and everything else in this section is just API calls.

***

## 1. The clinical network adapter

Tagada does not run doctors. It integrates **clinical networks** — companies that operate licensed provider groups and pharmacy routing. Every network is wrapped behind one adapter interface, so your integration code never changes when the network does.

```
Your storefront ──► Tagada Rx API ──► Clinical network adapter ──► Network (MDI, …)
                        │
                        └── rx_cases / rx_case_events (non-PHI only)
```

| What the adapter does                    | Example                                       |
| ---------------------------------------- | --------------------------------------------- |
| Create patient + case from an intake     | `rx.submitCase()` → network case `837858f1-…` |
| Normalize webhooks into canonical events | `case_approved` → `case.approved`             |
| Relay patient ↔ care-team messages       | portal thread                                 |
| Attach identity documents                | driver-license upload                         |
| Request refills                          | ship-day rebill cycle                         |

The active network is configured per store (`clinicalNetworkSlug`, default `mdi`). You can list what's available to your account with `GET /api/v1/rx/networks`.

***

## 2. PHI: transit, never storage

The single most important design rule:

<Warning>
  **Tagada never persists PHI.** Patient identity (name, DOB, address) and intake answers pass through the API to the clinical network in a single request and are not written to any Tagada table, queue, or log. What Tagada stores is routing data only: `rxcase_xxx`, the network's case id and patient id, statuses, timestamps, and non-PHI event metadata.
</Warning>

Practical consequences:

* **Case submission is synchronous** — there is no "queue the intake and retry later", because PHI must not sit in a queue. If the network is down, the storefront shows a retry UI.
* **Message bodies live at the network.** The patient portal relays threads directly from the clinical network; Tagada records only that a `message.created` event happened.
* **Emails are content-free.** "You have a new message from your care team" + a portal deep-link. Never the message text.

***

## 3. The webhook pipeline

The clinical network notifies Tagada of everything that happens after submission:

```
Network ──POST──► /api/public/webhooks/rx/{networkSlug}     (e.g. /rx/mdi)
                    │  1. resolve network + integration row
                    │  2. verify HMAC-SHA256 signature
                    │  3. normalize to canonical events
                    ▼
                  Inngest (async, retried, idempotent)
                    │
                    ├─ case.in_review / approved / declined … → status transition
                    ├─ shipment.shipped   → tracking + schedule next rebill
                    ├─ shipment.delivered → resting state
                    └─ message.created    → portal thread event + patient email
```

Key properties:

* **Signature-verified** — HMAC-SHA256 over the raw body, checked against the integration's `webhookSecret` before anything is processed.
* **Fast ACK** — the receiver verifies, normalizes, enqueues, and returns `200`. All database work happens asynchronously with retries.
* **Idempotent** — every event carries a synthesized `externalEventId`; duplicates are dropped at both the queue and the append-only event log.
* **Patient-scoped fallback** — message webhooks may arrive without a case id; Tagada resolves the case through the stored network patient id.

Your storefront never consumes these webhooks directly — it polls the public case-lookup endpoint or reads the portal API, both of which reflect the processed state.

***

## 4. The ship-day rebill engine

Classic subscription engines charge every N days from signup — which drifts from what the pharmacy actually does. Tagada Rx anchors billing to **fulfillment**:

<Steps>
  <Step title="Pharmacy ships">
    The `shipment.shipped` webhook lands. Tagada records the tracking number and stamps `lastShipmentShippedAt`.
  </Step>

  <Step title="Next charge is scheduled">
    `scheduledNextChargeAt = ship day + cycle` (default **30 days**, configurable per offering).
  </Step>

  <Step title="Hourly scan">
    A cron scans cases whose `scheduledNextChargeAt` is due **and** whose status is `shipped` or `delivered` — i.e. active subscribers only.
  </Step>

  <Step title="Refill fires">
    The rebill charges the stored payment instrument and requests the refill from the network. On the next shipment, the loop repeats.
  </Step>
</Steps>

<Note>
  Declined, cancelled, and errored cases never rebill. If a clinician declines mid-subscription, the commission accrued in affiliate mode is automatically reversed (see [Operating modes](/developer-tools/rx/operating-modes)).
</Note>

***

## Product mapping: what makes a product "Rx"

There is no flag on the product itself. A product requires a prescription when it has an **active offering mapping**:

```
Tagada product (product_xxx)  ──►  rx_product_offerings  ──►  network offering / SKU
```

Each mapping carries the clinical requirements the storefront needs to render the right flow:

| Field                    | Meaning                                              |
| ------------------------ | ---------------------------------------------------- |
| `externalOfferingId`     | The network's SKU for this treatment                 |
| `requiresLabs`           | Lab work needed before prescribing                   |
| `requiresIdVerification` | Photo ID must be uploaded (portal handles it)        |
| `isDeaControlled`        | Controlled substance — **blocked in affiliate mode** |

Storefronts ask at product load: `GET /api/public/v1/rx/required-for-product?productId=…` → `{ required, requiresLabs, requiresIdVerification, … }`, and only render the intake when `required` is true.

***

## Next

<CardGroup cols={2}>
  <Card title="Operating modes" icon="scale-balanced" href="/developer-tools/rx/operating-modes">
    MoR (affiliate) vs Direct Merchant.
  </Card>

  <Card title="End-to-end tutorial" icon="graduation-cap" href="/developer-tools/rx/tutorial">
    Build the whole flow in an afternoon.
  </Card>
</CardGroup>
