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

# Inventory API

> Read stock, post movements, reserve stock for orders and subscribe to stock events from your own systems.

The Inventory API lets your pharmacy system, fulfillment partner or storefront work with your inventory without opening the app: read what is on hand, resolve a scanned barcode, check stock in and out, reserve stock for an order, and get an event whenever stock changes.

<CardGroup cols={2}>
  <Card title="One rulebook" icon="shield-check">
    The same rules as the scan station: on-hand never goes negative, orders draw only from released, unexpired, unreserved lots first to expire first, and every change is one permanent ledger entry.
  </Card>

  <Card title="Safe to retry" icon="rotate">
    Every write carries an <code>Idempotency-Key</code>. A retry posts once.
  </Card>
</CardGroup>

## Authentication

Create a key in the app under **Inventory → Integrations**. A key belongs to your pharmacy and carries `inventory:read` and/or `inventory:write`. Send it as `Authorization: Bearer vr_live_...`.

* **Live keys** can read and write.
* **Test keys** (`vr_test_...`) can read but never change stock: there is no sandbox for inventory.
* Keys can be revoked at any time and are rate limited per key; `429 rate_limited` includes a `Retry-After` header.

## Posting a movement

```bash theme={null}
curl -X POST https://vitarelay.com/api/public/v1/inventory/movements \
  -H "Authorization: Bearer vr_live_..." \
  -H "Idempotency-Key: ord-48213-line-1" \
  -H "Content-Type: application/json" \
  -d '{"type":"fulfill","sku_code":"SEMA-5MG-2ML","location":"Main","qty":2,
       "reference":{"type":"order","id":"48213","rx_number":"RX-901244"}}'
```

`type` is `receive`, `fulfill`, `dispense` or `return`. A `fulfill` or `dispense` without a `lot_number` draws first-expiry-first-out and can return several transactions. Adjustments, waste, counts and transfers are done in the app by a person, because some need a witness.

## Rule failures

Failures return `{ "error": { "code", "message" } }` with the reason in plain English.

| Status | Code | Meaning |
| - | - | - |
| 409 | `insufficient_stock` | Not enough released, unexpired, unreserved stock |
| 422 | `lot_expired` | The lot is past its expiry or beyond-use date |
| 422 | `lot_not_released` | The lot is quarantined, on hold, recalled or destroyed |
| 422 | `lot_expiry_mismatch` / `expiry_required` | The expiry does not match the lot, or a new lot needs one |
| 404 | `product_not_found` / `location_not_found` / `lot_not_found` | Nothing matches |
| 400 | `idempotency_key_required` / `validation_error` / `lot_required` / `location_required` / `invalid_movement` | The request is incomplete or not allowed |
| 403 | `test_key_read_only` / `not_pharmacy` / `insufficient_scope` | The key cannot do this |
| 503 | `inventory_not_ready` | Inventory is not set up for your pharmacy yet |

## Webhooks

Subscribe in the app or with `POST /inventory/webhooks`. The URL must be public `https`. Each request is signed:

* `X-Webhook-Signature: sha256=<hex>`: HMAC-SHA256 of the **exact raw body**, keyed with your endpoint secret (shown once)
* `X-Webhook-Event`, `X-Webhook-Delivery` (unique per event, use it to deduplicate) and `X-Webhook-Timestamp`

Delivery is at least once. A failed delivery is retried with exponential backoff for about a day, then parked; you can replay it from the app.

| Event | Fires when |
| - | - |
| `stock.changed` | Any posted movement: SKU, lot, location, balance after, on hand and available |
| `stock.low` | A product's on-hand crossed its reorder point downward |
| `lot.status_changed` | A lot was released, held, quarantined, recalled, expired or destroyed |
| `lot.expiring` | A lot with stock reached its 90, 60 or 30 day window (once per window) |
| `sku.created` / `sku.updated` | A product was added or changed, including its availability |
| `count.approved` | A cycle count was approved |
| `temperature.excursion` | A refrigerator reading was outside 2 to 8 °C |

Storefronts should show availability from `stock.changed` (`available` is on hand minus reserved, released and unexpired only) rather than polling.

```js theme={null}
// Node: verify a delivery
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-webhook-signature"]));
```

See the **Inventory API Reference** for every endpoint and schema.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.