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

# Receiving Deliveries

> Build an endpoint that verifies, acknowledges, and processes VitaRelay webhook deliveries.

# Receiving Deliveries

Your endpoint is the **inbound** side of VitaRelay's outbound webhooks: VitaRelay
`POST`s events to the URL a VitaRelay admin configured for your organization, and
your server receives them.

<Note>
  You cannot register or edit endpoints through the API. A VitaRelay admin sets
  the URL and subscribed events in the dashboard and shows you the signing secret
  once.
</Note>

## Endpoint requirements

<Steps>
  <Step title="Accept POST over HTTPS">
    The body is JSON; read the **raw bytes** before parsing.
  </Step>

  <Step title="Verify the signature first">
    Compare against `X-VitaRelay-Signature` before doing anything else. See
    [Security](/webhooks/security).
  </Step>

  <Step title="Dedupe on X-VitaRelay-Delivery">
    Retries reuse the same delivery id. Skip work you've already done.
  </Step>

  <Step title="Return 2xx fast">
    Acknowledge within a few seconds; do heavy work asynchronously.
  </Step>
</Steps>

## Minimal handler

```typescript theme={null}
import crypto from "node:crypto";

const SECRET = process.env.VITARELAY_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  const rawBody = await request.text();
  const header = request.headers.get("X-VitaRelay-Signature");
  if (!header) return new Response("Missing signature", { status: 401 });

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(rawBody)
    .digest("hex");
  const received = header.replace(/^sha256=/, "");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return new Response("Invalid signature", { status: 401 });
  }

  const deliveryId = request.headers.get("X-VitaRelay-Delivery")!;
  if (await alreadyProcessed(deliveryId)) {
    return new Response("OK", { status: 200 });
  }

  const event = JSON.parse(rawBody);
  await enqueue(deliveryId, event); // process async
  return new Response("OK", { status: 200 });
}
```

## Routing by event type

```typescript theme={null}
switch (event.event) {
  case "order.paid":
    await markPaid(event.order_id, event.total_cents);
    break;
  case "order.shipped":
  case "order.delivered":
    await setTracking(event.order_id, event.status, event.tracking_number);
    break;
  case "order.exception":
    await flagDeliveryProblem(event.order_id, event.tracking_status);
    break;
  case "intake.reviewed":
    await markRxSigned(event.escript_id, event.patient_id, event.signed_at);
    break;
}
```

<Warning>
  Do not run long fulfillment, billing, or EHR writes inline. Slow handlers time
  out, which triggers retries and can eventually dead-letter the delivery.
</Warning>

## Testing

Point a sandbox-configured endpoint at your handler and use a `vr_test_` key.
Sandbox objects advance through a simulated lifecycle and fire the same events
with `"sandbox": true`. See [Sandbox](/getting-started/sandbox).
