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

# Sandbox

> Test the full API with a vr_test_ key — isolated, synthetic, and never charged.

# Sandbox

The sandbox lets you build and test the entire integration without touching
production data. It is not a separate host: you send requests to the **same base
URL** with a `vr_test_` key.

```
https://vitarelay.com/api/public/v1
```

## What a test key does

* **Nothing real is created.** No patients, no intakes, no orders are written to
  production tables.
* **No card is ever charged.** Payment is simulated.
* **Validation is identical to live.** Same required fields, same request and
  response shapes, same error codes, same idempotency behavior.
* **Responses are synthetic.** Objects come back with sandbox identifiers and a
  `"sandbox": true` marker.

## Synthetic identifiers

| Object  | Sandbox id      |
| ------- | --------------- |
| Patient | `sandbox_pat_…` |
| Intake  | `sandbox_int_…` |
| Order   | `sandbox_ord_…` |

```json theme={null}
{
  "data": {
    "id": "sandbox_pat_9f2c41d6",
    "external_patient_id": "clinic-patient-001",
    "sandbox": true
  }
}
```

In live traffic these ids are UUIDs. Sandbox ids are the only non-UUID
identifiers the API returns.

<Note>
  When referencing a sandbox patient from an intake or order, pass either the
  `external_patient_id` you supplied or the returned `sandbox_pat_…` id. Both
  resolve.
</Note>

## Simulated lifecycle

Sandbox objects advance on a timer, a few seconds apart, and each transition
enqueues the same webhook event your live integration will receive — marked
`sandbox: true` in the payload.

| You create | Simulated events                                   |
| ---------- | -------------------------------------------------- |
| Intake     | `intake.reviewed`                                  |
| Order      | `order.paid` → `order.shipped` → `order.delivered` |

<Note>
  Webhooks only arrive if the clinic has a webhook endpoint configured and
  subscribed to those events. Endpoints are configured by a VitaRelay admin in
  the dashboard, not through the API. See
  [Webhooks Overview](/webhooks/overview).
</Note>

## Run the full loop in sandbox

<Steps>
  <Step title="Get a test key">
    Ask your VitaRelay representative for a `vr_test_` key with `patients:write`,
    `intakes:write`, and `orders:write`. See
    [API Keys](/getting-started/api-keys).
  </Step>

  <Step title="Create a patient">
    ```bash theme={null}
    curl https://vitarelay.com/api/public/v1/patients \
      -H "Authorization: Bearer vr_test_xxxxxxxxxxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{
        "external_patient_id": "clinic-patient-001",
        "first_name": "Jane",
        "last_name": "Smith",
        "dob": "1985-04-12",
        "email": "jane.smith@example.com",
        "phone": "+18135550100"
      }'
    ```

    Returns `201` with `data.id` = `sandbox_pat_…` and `data.sandbox` = `true`.
  </Step>

  <Step title="Submit an intake">
    ```bash theme={null}
    curl https://vitarelay.com/api/public/v1/intakes \
      -H "Authorization: Bearer vr_test_xxxxxxxxxxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{
        "external_intake_id": "clinic-intake-001",
        "external_patient_id": "clinic-patient-001",
        "category": "weight_management",
        "responses": {
          "chief_complaint": "Weight management",
          "current_medications": [],
          "allergies": ["penicillin"]
        }
      }'
    ```

    A few seconds later, an `intake.reviewed` event is delivered to your
    configured endpoint.
  </Step>

  <Step title="Place an order">
    ```bash theme={null}
    curl https://vitarelay.com/api/public/v1/orders \
      -H "Authorization: Bearer vr_test_xxxxxxxxxxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{
        "external_order_id": "clinic-order-001",
        "external_patient_id": "clinic-patient-001",
        "items": [
          { "product_id": "6f1a2b3c-4d5e-4f60-8a71-9b0c1d2e3f40", "quantity": 1 }
        ]
      }'
    ```

    Returns `201` with `data.id` = `sandbox_ord_…`. No charge is attempted.
  </Step>

  <Step title="Watch the order events arrive">
    Your endpoint receives `order.paid`, then `order.shipped`, then
    `order.delivered`, each signed exactly like a live event and each carrying
    `sandbox: true`.
  </Step>
</Steps>

## Idempotency in sandbox

Idempotency works the same way as in live. Replaying a request with the same
`external_patient_id`, `external_intake_id`, or `external_order_id` and the same
body returns the original sandbox object with `200` instead of `201`. Reusing an
external id with a *different* body returns `409 duplicate_request`.

## Errors in sandbox

Error codes and statuses are identical to live — including
`validation_failed`, `product_not_found`, `insufficient_scope`, and
`rate_limited`. See [Errors](/getting-started/errors).

<Warning>
  Sandbox and live never intersect. A `vr_test_` key cannot read or modify
  production records, and objects created in the sandbox are invisible to
  `vr_live_` keys.
</Warning>
