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

# Create an order

> **Vita Clinic only.** Requires the `orders:write` scope.

Prices are resolved from the catalog — the request body must not (and
cannot) contain prices or organization ids. Ordering a 503B product
returns `422 product_not_allowed`.

With a live key the clinic's card on file is charged synchronously:
success returns `201` with `status: "paid"`; a failure returns `402`
with code `card_required` or `payment_declined`. When platform billing
is in dry-run mode the order is returned with
`status: "awaiting_payment"` and `billing_dry_run: true`.

Set `billing_mode` to `patient_checkout` to create an unpaid order
and receive a `checkout` block for embedded Accept.js payment on your
own page (see POST /orders/{id}/pay). In that mode no clinic card is
required and the order stays unpaid until the nonce is submitted.

Supplying `external_order_id` makes the call idempotent.




## OpenAPI

````yaml /openapi.yaml post /orders
openapi: 3.1.0
info:
  title: VitaRelay API
  version: 2.0.0
  description: |
    The VitaRelay REST API.

    **Base URL:** `https://vitarelay.com/api/public/v1`

    ## Authentication
    All requests require a Bearer API key:

    ```text
    Authorization: Bearer vr_live_xxxxxxxxxxxxxxxx
    ```

    Keys are issued per organization. `vr_live_` keys act on production data.
    `vr_test_` keys operate a fully-isolated **sandbox** (see below).

    ## Pagination
    List endpoints are cursor-based. Pass `limit` (default 50, max 100) and
    `cursor` (the `next_cursor` from the previous page). When `next_cursor`
    is `null` there are no further pages. All resource ids are UUIDs.

    ## Idempotency
    Write endpoints accept your own external identifiers —
    `external_patient_id`, `external_intake_id`, `external_order_id`. These act
    as idempotency keys: replaying a request with the same external id returns
    the original resource (HTTP `200` instead of `201`) rather than creating a
    duplicate.

    ## Rate limiting
    Write endpoints are rate limited per API key — default **60 requests per
    minute** (fixed window). Exceeding the limit returns `429 rate_limited`
    with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
    `X-RateLimit-Reset` headers. Read endpoints are not rate limited in v1.

    ## Sandbox
    With a `vr_test_` key, writes never create real patients, intakes or orders
    and never charge a card. They return synthetic ids (`sandbox_pat_…`,
    `sandbox_int_…`, `sandbox_ord_…`) and include `"sandbox": true` in the
    response. Sandbox objects advance through a simulated lifecycle that fires
    real webhook deliveries to your endpoint (`intake.reviewed`, `order.paid` →
    `order.shipped` → `order.delivered`) with `"sandbox": true` in the payload.
    In sandbox, reference a patient by `external_patient_id` or by the returned
    `sandbox_pat_…` id.

    ## Webhooks
    Outbound webhooks are configured in the VitaRelay admin UI (there is no
    webhook management API). Events: `order.paid`, `order.shipped`,
    `order.delivered`, `order.exception`, `intake.reviewed`. Each delivery is
    signed with HMAC-SHA256 over the raw body and carries the headers
    `X-VitaRelay-Signature: sha256=<hex>`, `X-VitaRelay-Event`,
    `X-VitaRelay-Delivery` (event id) and `X-VitaRelay-Timestamp`. Failed
    deliveries are retried with exponential backoff and dead-lettered after the
    maximum attempts.
  contact:
    name: VitaRelay Support
    email: info@vitarelay.com
servers:
  - url: https://vitarelay.com/api/public/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Patients
    description: Patient records scoped to your organization.
  - name: Intakes
    description: Intake form submissions. Write access is Vita Clinic only.
  - name: Orders
    description: >-
      Orders and fulfillment status. Two billing modes: `card_on_file` (default)
      charges the clinic's stored card synchronously, and `patient_checkout`
      creates the order unpaid and returns a checkout block so the patient can
      pay on your own page via Authorize.Net Accept.js (see POST
      /orders/{id}/pay).
  - name: Products
    description: Catalog products available to your organization.
  - name: Prescriptions
    description: >-
      Prescriptions written by your organization (read only). Refer patients
      into the Internal Doctor Network (IDN). You refer; an IDN doctor evaluates
      and prescribes. Requires IDN activation and rx scopes.
  - name: Storefront
    description: >-
      Build and manage your clinic's patient storefront — settings, curated
      product listings and bundles. Requires the `shop:read` / `shop:write`
      scopes.
  - name: Subscriptions
    description: >-
      Patient-shop subscription lifecycle. Requires `subscriptions:read` or
      `subscriptions:write`.
paths:
  /orders:
    post:
      tags:
        - Orders
      summary: Create an order
      description: |
        **Vita Clinic only.** Requires the `orders:write` scope.

        Prices are resolved from the catalog — the request body must not (and
        cannot) contain prices or organization ids. Ordering a 503B product
        returns `422 product_not_allowed`.

        With a live key the clinic's card on file is charged synchronously:
        success returns `201` with `status: "paid"`; a failure returns `402`
        with code `card_required` or `payment_declined`. When platform billing
        is in dry-run mode the order is returned with
        `status: "awaiting_payment"` and `billing_dry_run: true`.

        Set `billing_mode` to `patient_checkout` to create an unpaid order
        and receive a `checkout` block for embedded Accept.js payment on your
        own page (see POST /orders/{id}/pay). In that mode no clinic card is
        required and the order stays unpaid until the nonce is submitted.

        Supplying `external_order_id` makes the call idempotent.
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
      responses:
        '200':
          description: Idempotent replay of an existing `external_order_id`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/OrderCreated'
        '201':
          description: Order created (and charged, unless billing is in dry-run).
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/OrderCreated'
                  sandbox:
                    type: boolean
                    description: Present and `true` when created with a `vr_test_` key.
        '400':
          $ref: '#/components/responses/InvalidBody'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/PatientNotFound'
        '409':
          $ref: '#/components/responses/DuplicateRequest'
        '422':
          $ref: '#/components/responses/OrderValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  schemas:
    OrderCreate:
      type: object
      required:
        - items
      description: Exactly one of `patient_id` or `external_patient_id` is required.
      properties:
        external_order_id:
          type: string
          description: Your identifier for this order. Acts as an idempotency key.
        patient_id:
          type: string
          description: Patient UUID, or a `sandbox_pat_…` id when using a test key.
        external_patient_id:
          type: string
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/OrderItemCreate'
        shipping:
          $ref: '#/components/schemas/ShippingAddress'
        payment_method_id:
          type: string
          format: uuid
          description: >-
            Patient saved card. Required when any line has
            `purchase=subscription` (`422 payment_method_required`).
        ship_to_address_id:
          type: string
          format: uuid
          description: >-
            Patient saved address for shippable subscription lines (defaults to
            the preferred address). Service-only lines never require shipping;
            otherwise `422 shipping_required`.
        billing_mode:
          type: string
          enum:
            - card_on_file
            - patient_checkout
          default: card_on_file
          description: >-
            `card_on_file` (default) charges the Vita Clinic's stored card
            synchronously. `patient_checkout` creates the order UNPAID and
            returns a `checkout` object so the patient can pay on the partner's
            own page via Authorize.Net Accept.js — no card is required on the
            clinic.
      example:
        external_order_id: clinic-order-8890
        external_patient_id: clinic-patient-001
        items:
          - product_id: 7c1f0f7e-9a2b-4f1d-9c33-1a2b3c4d5e6f
            quantity: 1
        shipping:
          line1: 22 Wellness Ave
          city: Tampa
          state: FL
          postal_code: '33602'
          country: US
    OrderCreated:
      type: object
      properties:
        id:
          type:
            - string
            - 'null'
          description: >-
            Order UUID (or a `sandbox_ord_…` id with a test key). `null` when
            every line is a subscription.
        order_id:
          type:
            - string
            - 'null'
          description: Same as `id`.
        subscription_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Present when the request contained subscription lines.
        order_number:
          type: string
        status:
          type: string
          description: >-
            `paid` after a successful charge, or `awaiting_payment` when billing
            is in dry-run.
        total_cents:
          type: integer
        external_order_id:
          type:
            - string
            - 'null'
        billing_dry_run:
          type: boolean
          description: Present and `true` when platform billing is in dry-run mode.
        checkout:
          type:
            - object
            - 'null'
          description: >-
            Present only for billing_mode=patient_checkout. Everything needed to
            mount Accept.js on the partner's page. Only the PUBLIC client key +
            API Login ID are returned (safe to expose to a browser); the
            Transaction Key is never included.
          properties:
            order_id:
              type: string
              format: uuid
            amount_cents:
              type: integer
            currency:
              type: string
            public_client_key:
              type: string
            api_login_id:
              type: string
            environment:
              type: string
              enum:
                - sandbox
                - production
            accept_js_url:
              type: string
              format: uri
      example:
        id: sandbox_ord_5c1a9b2e
        order_number: VR-88901
        status: paid
        total_cents: 12900
        external_order_id: clinic-order-8890
    OrderItemCreate:
      type: object
      required:
        - quantity
      description: >-
        Send either `product_id` (catalog product; unchanged legacy behaviour)
        or `item_kind` + `item_id` (a storefront item). Unknown fields are
        ignored. One-time purchase of a subscription-only storefront item
        returns `422 purchase_mode_not_allowed`. Subscription lines are created
        through the same subscription engine as the patient storefront.
      properties:
        product_id:
          type: string
          format: uuid
        item_kind:
          type: string
          enum:
            - product
            - custom
            - bundle
        item_id:
          type: string
          format: uuid
        quantity:
          type: integer
          minimum: 1
        purchase:
          type: string
          enum:
            - one_time
            - subscription
          default: one_time
        cadence:
          type: string
          enum:
            - weekly
            - monthly
            - quarterly
          description: >-
            Subscription lines only. Defaults to the item's default cadence;
            otherwise `422 invalid_cadence`.
    ShippingAddress:
      allOf:
        - $ref: '#/components/schemas/Address'
        - type: object
          properties:
            country:
              type: string
              default: US
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: |
                One of `invalid_body`, `validation_failed`, `unauthorized`,
                `insufficient_scope`, `not_vita_clinic`, `patient_not_found`,
                `product_not_found`, `product_not_allowed`, `duplicate_request`,
                `rate_limited`, `card_required`, `payment_declined`,
                `internal_error`, `not_found`.
            message:
              type: string
      example:
        error:
          code: validation_failed
          message: One or more fields are invalid
    Address:
      type: object
      required:
        - line1
        - city
        - state
        - postal_code
      properties:
        line1:
          type: string
        line2:
          type: string
        city:
          type: string
        state:
          type: string
          description: Two-letter US state code.
        postal_code:
          type: string
  headers:
    XRateLimitLimit:
      description: Requests permitted in the current window.
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
    XRateLimitReset:
      description: Unix timestamp when the current window resets.
      schema:
        type: integer
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
  responses:
    InvalidBody:
      description: >-
        Malformed request or invalid path parameter (`invalid_body`,
        `invalid_id`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, invalid or revoked API key (`unauthorized`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequired:
      description: |
        No card on file (`card_required`) or the charge was declined
        (`payment_declined`). The order is not fulfilled.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: |
        The key lacks the required scope (`insufficient_scope`) or its
        organization is not an active Vita Clinic (`not_vita_clinic`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PatientNotFound:
      description: >-
        Patient not found or owned by another organization
        (`patient_not_found`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    DuplicateRequest:
      description: >-
        A conflicting request with the same external id is in flight
        (`duplicate_request`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    OrderValidationFailed:
      description: |
        Validation failure (`validation_failed`), unknown product
        (`product_not_found`) or a 503B product that clinics may not order
        (`product_not_allowed`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Per-key rate limit exceeded (`rate_limited`). Default 60 writes/minute.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: Unexpected server error (`internal_error`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API key issued by VitaRelay. Send as `Authorization: Bearer vr_live_…`
        (production) or `Authorization: Bearer vr_test_…` (sandbox).

````