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

# Pharmacy Integration

> Push order fulfillment status and inventory updates from your pharmacy system into VitaRelay.

# Pharmacy Integration

This guide is for **partner pharmacies** connecting their own systems to VitaRelay. It documents the inbound webhook you call — pharmacy → VitaRelay — to push order fulfillment status, tracking, and inventory updates as they happen.

VitaRelay dispatches orders to your system through a separate, pharmacy-specific onboarding channel arranged directly with your VitaRelay contact. That channel is not documented here.

<CardGroup cols={2}>
  <Card title="Status updates">
    Move an assigned order through `accepted` → `delivered` and attach tracking.
  </Card>

  <Card title="Inventory updates">
    Keep stock availability and wholesale cost current for your products.
  </Card>
</CardGroup>

## Endpoint & authentication

POST your updates to:

```
POST https://vitarelay.com/api/public/pharmacy-webhook/{orgId}
```

`{orgId}` is your pharmacy's VitaRelay organization UUID, provided by a VitaRelay admin. `POST` is the only supported method, and the request must be sent with `Content-Type: application/json`.

Every request must be signed with the **inbound webhook secret**:

* Header: `x-vitarelay-signature`
* Value: HMAC-SHA256 of the **exact raw request body bytes**, keyed with your inbound webhook secret.
* Encoding: **hex or base64** are both accepted, and a leading `sha256=` prefix is tolerated (optional).

<Note>
  The inbound webhook secret is issued — and rotated — by a VitaRelay admin and shared with you directly. It is per-pharmacy, shown once, and cannot be retrieved self-serve. Store it in a secret manager.
</Note>

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  const secret = process.env.VITARELAY_INBOUND_WEBHOOK_SECRET;
  const orgId = process.env.VITARELAY_ORG_ID;

  // Serialize ONCE and sign the exact bytes you send.
  const rawBody = JSON.stringify({
    event: "status_update",
    order_number: "VR-20260804-118342",
    status: "shipped",
    tracking_number: "1Z999AA10123456784",
    tracking_carrier: "ups",
    tracking_url: "https://www.ups.com/track?tracknum=1Z999AA10123456784",
  });

  const signature = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const res = await fetch(
    `https://vitarelay.com/api/public/pharmacy-webhook/${orgId}`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-vitarelay-signature": signature,
      },
      body: rawBody,
    },
  );

  console.log(res.status, await res.json());
  ```

  ```bash cURL theme={null}
  BODY='{"event":"status_update","order_number":"VR-20260804-118342","status":"shipped","tracking_number":"1Z999AA10123456784","tracking_carrier":"ups"}'
  SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$VITARELAY_INBOUND_WEBHOOK_SECRET" -hex | awk '{print $2}')

  curl -X POST "https://vitarelay.com/api/public/pharmacy-webhook/$VITARELAY_ORG_ID" \
    -H "Content-Type: application/json" \
    -H "x-vitarelay-signature: $SIG" \
    -d "$BODY"
  ```
</CodeGroup>

<Warning>
  Sign the exact bytes you transmit. If you re-serialize the body after signing — pretty-printing, reordering keys, or letting an HTTP client rebuild the JSON — the signature will not match and the request is rejected with `401`.
</Warning>

## Sending a status update

Use `event: "status_update"` to update the fulfillment status of a single order assigned to your pharmacy.

```json theme={null}
{
  "event": "status_update",
  "order_number": "VR-20260804-118342",
  "status": "shipped",
  "tracking_number": "1Z999AA10123456784",
  "tracking_carrier": "ups",
  "tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
}
```

| Field              | Type   | Required | Notes                                                                               |
| ------------------ | ------ | -------- | ----------------------------------------------------------------------------------- |
| `event`            | string | yes      | Must be `status_update`                                                             |
| `order_number`     | string | yes      | The VitaRelay order number. A trailing `-r<N>` retry suffix, if present, is ignored |
| `status`           | string | no       | One of `accepted`, `processing`, `ready_to_ship`, `shipped`, `delivered`            |
| `tracking_number`  | string | no       | Carrier tracking number                                                             |
| `tracking_carrier` | string | no       | Carrier code, e.g. `ups`, `fedex`                                                   |
| `tracking_url`     | string | no       | Must be a valid URL                                                                 |

Only those four fields — `status` plus the three tracking fields — are applied, and only to the fulfillment record for **your** pharmacy's assignment on that order. You can only update orders assigned to you. Any `status` value outside the enum above is rejected with `400`.

The typical progression is `accepted` → `processing` → `ready_to_ship` → `shipped` → `delivered`. VitaRelay does not enforce a transition state machine; it applies whatever valid status you send.

## Sending an inventory update

Use `event: "inventory_update"` to refresh stock availability and wholesale cost for your products.

```json theme={null}
{
  "event": "inventory_update",
  "products": [
    { "ndc_code": "12345-6789-01", "in_stock": true, "wholesale_cost_cents": 4200 },
    { "name": "Semaglutide 2.5mg/mL", "in_stock": false }
  ]
}
```

| Field                             | Type    | Required | Notes                                         |
| --------------------------------- | ------- | -------- | --------------------------------------------- |
| `event`                           | string  | yes      | Must be `inventory_update`                    |
| `products`                        | array   | yes      | Maximum 500 items per request                 |
| `products[].ndc_code`             | string  | no       | Preferred match key                           |
| `products[].name`                 | string  | no       | Fallback match key when no `ndc_code` is sent |
| `products[].in_stock`             | boolean | no       | Stock availability                            |
| `products[].wholesale_cost_cents` | integer | no       | Must be ≥ 0                                   |

Each item is matched by `ndc_code` when present, otherwise by `name`. Items with neither key are skipped. Only the fields you provide are updated, and only for products belonging to your pharmacy.

## Responses & error codes

| Status | Body                            | Meaning                                                                           |
| ------ | ------------------------------- | --------------------------------------------------------------------------------- |
| `200`  | `{ "ok": true, "note": "..." }` | Request accepted. See the `note` for what actually happened                       |
| `400`  | `Invalid org id`                | The `{orgId}` in the URL is not a valid UUID                                      |
| `404`  | `Integration not enabled`       | No integration record, integration disabled, or no secret configured for that org |
| `401`  | `Invalid signature`             | The signature header is missing or does not match                                 |
| `400`  | `Invalid payload: <reason>`     | Schema validation failed — unknown event, bad status value, invalid URL, etc.     |

Example `note` values on a `200`:

```json theme={null}
{ "ok": true, "note": "order VR-20260804-118342 updated" }
{ "ok": true, "note": "order VR-20260804-118342 not found" }
{ "ok": true, "note": "2 product(s) updated" }
```

<Warning>
  A `200` response is **not** proof that your order matched. An unknown `order_number` still returns `200`, with the note `order ... not found`. Always inspect the `note` field and alert on the `not found` case.
</Warning>

## Send your first signed update

<Steps>
  <Step title="Get your credentials">
    Ask your VitaRelay contact for your organization UUID and your inbound webhook secret. Store the secret in a secret manager — it is shown only once.
  </Step>

  <Step title="Confirm your integration is enabled">
    A VitaRelay admin must enable your integration record. Until then, the endpoint returns `404 Integration not enabled`.
  </Step>

  <Step title="Build and sign the body">
    Serialize your JSON payload once, compute `HMAC-SHA256(rawBody, secret)`, and send the hex digest in `x-vitarelay-signature`.
  </Step>

  <Step title="POST and verify">
    Send the request to `https://vitarelay.com/api/public/pharmacy-webhook/{orgId}`. Confirm you receive `200` and that the `note` reports the order or product count you expected.
  </Step>

  <Step title="Go live">
    Wire the call into your fulfillment workflow so each status change and inventory refresh is pushed to VitaRelay as it happens. VitaRelay notifies the practice and patient automatically on each status update.
  </Step>
</Steps>
