---
title: "Send an order"
description: "Create, sign and submit an order to the pharmacy with one POST /v1/orders request."
---

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

# Send an order

One `POST /v1/orders` request creates the order, signs it for the prescriber and sends it to the
pharmacy. There is no draft, sign or submit step in the API.

A signed order keeps its medication, shipping service and dispensing pharmacy.
Later catalog changes do not change what the pharmacy receives.
Order reads keep the shipping option name and price saved at signing.

An order has one practice, one patient, one prescriber and 1 to 20 prescriptions. All prescriptions go to one pharmacy.
Each prescription returns its pharmacy, status and nullable failure reason.
PerfectRx prescriptions use the same status and failure reason as their order.
All prescriptions in one order must go to one pharmacy. Send a separate order for each pharmacy.
The same medication may appear more than once. Each line is a separate prescription with its own price.
To order more than one fill of a medication, send one line with a larger `quantity`.

Orders use `/v1/orders`, not a route under a practice.
Platform keys must send `practiceId`. Missing `practiceId` returns `422` with the path `practiceId`.
A practice key can omit `practiceId`; Affinity uses that practice.
An archived or unknown practice returns `404 not_found` and cannot take new orders. Use an active practice that your organization owns.

Schema validation runs before ordering rules. A `400` response can list missing body fields before
the API checks `practiceId` and `prescriber`. After those fields are corrected, a platform request
without `practiceId`, or any API key request without `prescriber`, returns `422` `invalid_order`.
Send these fields with the first request.
A practice key that names another practice gets `404`.
The order list still accepts `GET /v1/orders?practiceId=…`.

## Before you send

- Get the clinician's approval of this exact order in your product.
- [Choose a medication](/guides/choose-a-medication/) for each prescription.
- Make sure that the practice has an address and a phone number. The pharmacy needs them. Without
  them, the order returns `422` with the path `practice.address` or `practice.phone`.

## Prescriber contact details

Send the prescriber's NPI and name in `prescriber`.
You can also send `prescriber.phone` and `prescriber.address`.
Each omitted contact field uses the practice's corresponding value.
A `null` or blank `prescriber.phone` also uses the practice phone.
Affinity checks the NPI format and check digit only.
It does not check the name against the NPI registry.
It does not look up an account by NPI.

App orders use the practice's address and phone. The member's phone and address in Settings → Profile
do not go to the pharmacy.

Affinity stores the effective contact details when it signs the order.
Later practice edits do not change that snapshot.

## Send the request

Use this synthetic example in Test mode. Replace `practiceId` with your practice ID.
Use [Test outcomes](/guides/test-and-live-mode/#test-outcomes) to test a failed order or a refused cancel.
Read the [current catalog](/guides/choose-a-medication/) before you send the request.
Use the medication ID, an accepted `quantity`, and a shipping option ID from the same catalog item.
This example uses PerfectRx Tirzepatide / B12 30 mg/0.5 mg/mL, quantity 6 (6 vials of 2 mL),
and Ground Cold Chain shipping.
Test and Live use the same medication and shipping option IDs, names and default prices.
Copy `shippingOptions[].id` from your `GET /v1/medications` response into `shipping.optionId`.
Do not reuse a shipping option ID from these docs.
The prescription directions and compounding reason are synthetic Test data. Use clinician-approved values for Live orders.
Save the JSON body as `order.json`.

```sh
curl https://api.affinityrx.com/v1/orders \
  -H "Authorization: Bearer $AFFINITY_API_KEY" \
  -H "Affinity-Version: 2026-10-28" \
  -H "Idempotency-Key: order-cuvo-123" \
  -H "Content-Type: application/json" \
  --data @order.json
```

```json
{
  "practiceId": "prac_01j9z3k4m5n6p7q8r9s0t1v2w3",
  "externalOrderId": "cuvo-123",
  "prescriber": {
    "npi": "1234567893",
    "firstName": "Sam",
    "lastName": "Lee",
    "phone": "+15125550124",
    "address": {
      "line1": "300 Example St",
      "line2": null,
      "city": "Austin",
      "state": "TX",
      "postalCode": "78701"
    }
  },
  "signatureAttestation": true,
  "shipping": { "optionId": "<shipping option id from GET /v1/medications>" },
  "patient": {
    "externalId": "patient-88",
    "firstName": "Test",
    "middleName": null,
    "lastName": "Patient",
    "dateOfBirth": "1988-04-02",
    "sex": "female",
    "phone": "+15125550100",
    "email": "test.patient@example.com",
    "address": {
      "line1": "100 Example St",
      "line2": null,
      "city": "Austin",
      "state": "TX",
      "postalCode": "78701"
    },
    "allergies": [
      {
        "display": "Penicillin",
        "category": "drug"
      }
    ]
  },
  "prescriptions": [
    {
      "medicationId": "med_3v9r73nmfs8bmbjb960yd2pggg",
      "quantity": 6,
      "refills": 0,
      "directions": "Inject 0.25 mL (7.5 mg tirzepatide) under the skin once weekly.",
      "compoundingReason": "concentration_adjustment",
      "notes": null,
      "substitutionPermitted": true
    }
  ]
}
```

## The fields

| Field                  | Rule                                                                                                                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `practiceId`           | Required with a platform key. Use a child practice ID. Independent practice keys can omit this field to use their own practice.                                                                                          |
| `externalOrderId`      | Optional. Your ID for the order, up to 128 characters. Events and lists return it. A second order with the same value for the practice returns `409` `order_already_exists`. |
| `prescriber`           | Required with an API key. The NPI and name of the clinician who signs.                                                                                                                                                   |
| `signatureAttestation` | Required. Send `true`. It states that the named prescriber approved and signed this order in your product.                                                                                                               |
| `shipping.optionId`    | Required. A shipping option from the medication's `shippingOptions`.                                                                                                                                                     |
| `patient`              | Required. The patient as the pharmacy must see them.                                                                                                                                                                     |
| `prescriptions`        | Required. 1 to 20 prescriptions.                                                                                                                                                 |

Affinity trims surrounding whitespace from text fields and external IDs. It validates an NFC copy.
Length limits count Unicode code points in that copy. Affinity stores the source after trimming.
External IDs keep exact identity, including case and normalization form.
The `externalOrderId` list filter uses the same rule as order creation.
An ID that is blank after trimming is refused. Names, addresses and external IDs accept internal tabs.
Other control and format characters are refused in single-line fields.
Email and allergy descriptions refuse internal tabs. Email also refuses format characters, including the soft hyphen U+00AD.

The patient:

- Send `firstName`, `lastName`, `dateOfBirth` (`YYYY-MM-DD`), `sex` (`female`, `male` or
  `unknown`), `phone` (a US phone, such as `(512) 555-0100` or `+15125550100`), `email` and `address`.
  See [Input formats](/guides/#input-formats) for the phone, state and ZIP forms you can send.
- `middleName` and `address.line2` are optional. Omit them, or send `null` or empty text, when there is no value.
- Send `allergies` as `[{ display, category }]`. `category` is one of `drug`, `food`, `insect`,
  `latex`, `mold`, `pet` or `pollen`. An empty list means "no new allergies reported".
  PerfectRx keeps allergies it already holds when you send `allergies: []`.
- Send `externalId`, your ID for the patient. Affinity finds the patient again by `externalId` in
  the practice and mode, and updates the patient from each order. Without `externalId`, each order
  adds a new patient.

Reusing `patient.externalId` updates the patient's details. Earlier orders keep their original patient
snapshots. The first signed PerfectRx order reserves the patient's `firstName` and `lastName`,
including while that order waits to reach the pharmacy. If every pending order is confirmed cancelled
or fails before PerfectRx accepts the patient, a replacement order can use a corrected name.
Once PerfectRx accepts the patient, the name stays reserved even if the order is rejected or cancelled.
Sending a different reserved name returns `422`
with an issue on the changed field before Affinity updates the patient or signs another order.
Case and whitespace differences, including spaces inside a name, are accepted. Accents remain part
of the stored name. When matching an existing patient across practices, Affinity also tolerates
accent differences such as "José" / "Jose"; the matched pharmacy name remains authoritative.
PerfectRx keeps the original demographic name even when
its update endpoint reports success. Contact Affinity support to correct the name before sending
another order. Use a new `externalId` for a different person, never to bypass a correction for the
same patient.

PerfectRx can share one matched patient record across practices and platforms. Later orders update
that record's contact details.

Each prescription:

- Send `medicationId`, `quantity`, `refills` (0 to 99) and `directions`.
- If `quantities` is not empty, use one of its values. Do not assume that the quantity is `1`.
- For a vial product, `quantity` counts vials: `quantity: 1` is one vial and `quantity: 6` is 6 vials.
  Send 1 to 10. The prescription price is the medication `price` times the number of vials.
  For tablets, capsules and creams, `quantity` counts `quantityUnit` and is a whole number of fills.
  Read [Fill, quantity and strength](/guides/choose-a-medication/#fill-quantity-and-strength).
- Send `compoundingReason` when the medication's `requirements` contains it. Use one of the
  medication's `compoundingReasons`, for example `concentration_adjustment`. Another value returns
  `422` `invalid_order` with the accepted values.
- `notes` is optional. `substitutionPermitted` is optional and is `true` when you do not send it.

`directions` and `notes` accept tabs and line breaks. Affinity preserves internal tabs when it sends these fields to PerfectRx.
Tabs stay unchanged in other text fields that permit them, including names and addresses.

`refills` is the total number of refills the prescriber authorizes at the pharmacy, in addition to the first fill.
Affinity sends this authorization to the pharmacy. Affinity does not request refills.
If your platform sends a new order for each monthly fill, send `refills: 0` on each order.
For example, send one order in January and a new order in February, each with a new `externalOrderId` and `Idempotency-Key`.
This avoids authorizing extra pharmacy refills alongside the monthly orders.

The order keeps the patient and the prescriber as you sent them. There are no patient or prescriber
endpoints.

A cancelled order keeps its `externalOrderId` in use. Reusing it returns `409 order_already_exists`; use a new ID for a replacement.

## Where the order ships

Send the shipping option ID in `shipping.optionId`.
If you omit `shipping.destination`, the pharmacy ships to the patient's address.
Choose a destination with its `type`:

- `{ "type": "patient" }` uses the patient's address.
- `{ "type": "practice" }` uses the practice address on file at the pharmacy. Affinity updates that address before sending.
- `{ "type": "address", "address": { ... } }` uses another address. The patient address rules apply.

Only the `address` type accepts an `address` field. An address on another type returns `400 invalid_request`.
The patient address remains required for the pharmacy's patient record.

`restrictedStates` and pharmacy coverage use the destination's state.
A refusal names `patient.address.state`, `practice.address.state` or `shipping.destination.address.state`.

To use the practice address, replace the request's `shipping` object with:

```json
{
  "optionId": "<shipping option id from GET /v1/medications>",
  "destination": { "type": "practice" }
}
```

To use another address, replace the request's `shipping` object with:

```json
{
  "optionId": "<shipping option id from GET /v1/medications>",
  "destination": {
    "type": "address",
    "address": {
      "line1": "55 Other Location Rd",
      "line2": "Suite 4",
      "city": "Columbus",
      "state": "OH",
      "postalCode": "43215"
    }
  }
}
```

The response returns `shipping.option` with `id`, `name` and `price`, or `null` for a historical order without an option.
It always returns `shipping.destination`, including `{ "type": "patient" }` when you omitted the destination.

## Retry safely

`Idempotency-Key` is required. A missing key returns `400 invalid_request`.
Send a new key for each new order. When a request times out, send it again with the
same key and the same body. Affinity does not create a second order. It returns `200` with the order
that the first request created, as it is now. A request refused with `400` or `422` does not use up
its key. Read [Retry safely](/guides/#retry-safely) for the rules that apply to every request.

Save the key with the order before the first request, and send it in the `Idempotency-Key` header every time you retry that order.

## The response

A created order returns `200` with the whole order:

```json
{
  "id": "ord_01j9z3k4m5n6p7q8r9s0t1v2w7",
  "externalOrderId": "cuvo-123",
  "mode": "test",
  "status": "submitted",
  "statusHistory": [
    {
      "status": "submitted",
      "createdAt": "2026-10-20T15:04:05.000Z"
    }
  ],
  "failureReason": null,
  "cancelRequestedAt": null,
  "trackingUrl": null,
  "practiceId": "prac_01j9z3k4m5n6p7q8r9s0t1v2w3",
  "practice": {
    "archivedAt": null,
    "name": "Example Clinic",
    "phone": "+15125550123",
    "address": {
      "line1": "200 Example Ave",
      "line2": "Suite 4",
      "city": "Austin",
      "state": "TX",
      "postalCode": "78701"
    }
  },
  "patientId": "pat_01j9z3k4m5n6p7q8r9s0t1v2w8",
  "prescriber": {
    "npi": "1234567893",
    "firstName": "Sam",
    "lastName": "Lee",
    "phone": "+15125550124",
    "address": {
      "line1": "300 Example St",
      "line2": null,
      "city": "Austin",
      "state": "TX",
      "postalCode": "78701"
    }
  },
  "patient": {
    "firstName": "Test",
    "middleName": null,
    "lastName": "Patient",
    "dateOfBirth": "1988-04-02",
    "phone": "+15125550100",
    "email": "test.patient@example.com",
    "address": {
      "line1": "100 Example St",
      "line2": null,
      "city": "Austin",
      "state": "TX",
      "postalCode": "78701"
    },
    "allergies": [
      {
        "display": "Penicillin",
        "category": "drug"
      }
    ],
    "sex": "female"
  },
  "prescriptions": [
    {
      "id": "rx_01j9z3k4m5n6p7q8r9s0t1v2w9",
      "medication": {
        "id": "med_3v9r73nmfs8bmbjb960yd2pggg",
        "name": "Tirzepatide / B12",
        "strength": "30 mg/0.5 mg/mL",
        "form": "Vial",
        "quantityUnit": "vial",
        "fill": {
          "amount": 1,
          "container": "vial",
          "ingredients": [
            {
              "amount": 60,
              "name": "Tirzepatide",
              "unit": "mg"
            },
            {
              "amount": 1,
              "name": "B12",
              "unit": "mg"
            }
          ],
          "unit": "vial",
          "volume": {
            "amount": 2,
            "unit": "mL"
          }
        }
      },
      "quantity": 6,
      "refills": 0,
      "directions": "Inject 0.25 mL (7.5 mg tirzepatide) under the skin once weekly.",
      "compoundingReason": "concentration_adjustment",
      "notes": null,
      "substitutionPermitted": true,
      "price": 70350,
      "pharmacy": {
        "id": "pharm_314xqfy8g09snbzq56416t1zb1",
        "name": "PerfectRx Pharmacy"
      },
      "status": "submitted",
      "failureReason": null
    }
  ],
  "shipments": [],
  "total": 73056,
  "createdAt": "2026-10-20T15:04:05.000Z",
  "updatedAt": "2026-10-20T15:04:05.000Z",
  "shipping": {
    "option": {
      "id": "shp_7yna882w139zfbmwzeyb16y24e",
      "name": "Ground Cold Chain",
      "price": 2706
    },
    "destination": {
      "type": "patient"
    }
  }
}
```

Keep the order `id`. All prices are integers in cents.
A prescription's `medication.fill` is the fill at signing. Later catalog changes do not change it.
Order creation uses the target practice's prescription and shipping prices.
Read [Prices for your practice](/guides/choose-a-medication/#prices-for-your-practice) before you show catalog prices to that practice.

`total` is the sum of these amounts:

- Prescription prices
- Shipping price, including packing and supplies

Each option price includes `shippingSuppliesPrice`; a mixed order pays the highest supplies price once. Example: $25.00 cold chain + $2.06 supplies = $27.06.

The order keeps its prices. Later catalog price changes do not change the order or its invoice.

## When the order fails validation

- A missing or malformed field returns `400` `invalid_request`.
- A field that breaks a rule returns `422` `invalid_order`. Examples: a required compounding
  reason, a restricted state, a shipping option that the pharmacy does not offer.

Both responses list each problem in `issues`, with its `field` and `path`.
Each validation message explains the expected type, allowed values, format or limit. For example,
`patient.sex` lists `female`, `male` and `unknown`; `practiceId` expects a `prac_` ID.
`Idempotency-Key` must contain 1 to 255 printable ASCII characters
without spaces. Read [Errors](/errors/).

## What happens next

| Status | Meaning |
| --- | --- |
| `submitted` | Affinity accepted the order. In Live, it can stay submitted while Affinity resolves a pharmacy problem. |
| `processing` | The pharmacy has the order. It received, accepted or compounds it. |
| `shipped` | At least one package left the pharmacy. Read `shipments`. |
| `delivered` | The pharmacy reports that the carrier delivered it. Some pharmacies never report this status. |
| `cancelled` | The pharmacy confirmed the cancel, or cancelled the order itself. |
| `failed` | The order could not proceed. Read failureReason; a Live order failed by staff carries their written reason. |

A pharmacy can skip a status. For example, an order can go from `submitted` to `shipped`.
`failed` and `delivered` are final. A `failed` order has a `failureReason` for a person to read.
To fix a failed order, send a new order.

Temporary database failures retry automatically in Test and Live mode.
If a pharmacy answer is lost, routine status checks and verified pharmacy updates can recover the order.
Affinity checks the original pharmacy reference before it sends an uncertain order again.

In Live mode, an order the pharmacy refused or Affinity could not send stays `submitted` while
Affinity staff resolve the problem. This is a held order. A platform cannot distinguish a held
order from a slow order through the API.
Affinity staff can resend the order or set it to `failed` with a written reason in `failureReason`.
A patient-record mismatch still fails promptly and tells you which details to check.
Read `failureReason` before you send a replacement order.

In Test mode, a pharmacy refusal fails the order promptly.
Retryable failures get up to 3 attempts.
When a transmit may have started, Affinity checks the original pharmacy reference before sending again.
Exhausted attempts fail the order and send `order.failed`. This also applies when a transmit may have
reached the Test pharmacy, because Test has no staff queue.

A pharmacy can ship after it confirms cancellation. A `cancelled` order can then become `shipped` or `delivered`.
Continue to apply newer webhook events. Check the latest order before you send a replacement.

Each status change sends a webhook event. Read [Receive order updates](/guides/receive-order-updates/).

### PerfectRx field limits

For an order routed to PerfectRx, each patient, practice and prescriber address must have at
most 70 characters across `line1` and `line2` combined. Prescriber and patient first and last
names must each have at most 50 characters. The patient's full contact name, including the
optional middle name and separating spaces, must have at most 100 characters.
Practice names must have at most 100 characters. Practice save and order preflight enforce this limit.

Each patient first and last name must include at least one letter A-Z, in either case.
A name without one returns `422 invalid_order` with `patient.firstName` or `patient.lastName`.

Affinity checks these PerfectRx requirements before signing. An over-limit order returns
`422 invalid_order` with the affected field and limit. Correct the source value and resend;
Affinity does not shorten names or addresses. These limits apply only to PerfectRx.

PerfectRx accepts Windows-1252 text, including `é ñ ü ç ’ “ ” – — €`. Affinity normalizes
transmitted text to NFC, so a decomposed accent such as the letter e plus U+0301 becomes `é`.
The stored signed text stays unchanged.
Unsupported characters, such as `ł`, `ő`, Hangul, Greek,
Cyrillic, CJK, emoji and thin spaces, return `422` with the affected field and the first
unsupported character. Correct the source text and resend. Practice names and addresses
use the same rule when saved because practices can order from PerfectRx.
A refused combining mark is named by code point, such as `U+0301 COMBINING ACUTE ACCENT`.
Invisible characters, including Hangul fillers, use a code point such as `U+3164`.

Raw `<` and `>` are also refused. Write "less than" or "more than" instead. Quotes and
`&` are sent directly; do not replace them with HTML escapes.

## Prescription events

Order events include `data.order.prescriptions`, with `id`, `status` and `failureReason` for each prescription.
`prescription.failed` and `prescription.cancelled` identify one changed prescription through `data.prescriptionId`.
They use the same `data.order` payload as order events.
PerfectRx sends no separate prescription events because all prescriptions move with their order.
Handle unknown event types and new status values without stopping event processing.

Source: https://docs.affinityrx.com/guides/send-an-order/index.mdx

If a response or a page is wrong or unclear, send `POST /v1/feedback` with an `Idempotency-Key`, a short message and the `requestId` from the response. Do not include patient information.
