---
title: "Build a headless integration"
description: "Register clinicians, prepare orders, and sign and submit from your backend."
---

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

# Build a headless integration

Your platform owns clinician authentication, prescription review, and collection of signing intent.
Affinity records the platform's assertion and validates prescribing authority, prescription versions, and fulfillment eligibility.
The clinician does not need to sign in to Affinity for the headless workflow.

## Start in Test mode

Sign up at [Affinity for Platforms](https://platform.affinityrx.com).
Test access is available immediately after setup. Create a Test API key on your backend.
Platform Live access requires Affinity approval. Approved platforms can enable or disable Live access for their own practices.
Set `liveEnabled: true` when creating a practice to enable access immediately with a Live key and `practices:write`.
See [Manage practice Live access](/guides/test-and-live-mode/#manage-practice-live-access) for updates and Admin restrictions.

Use `team:write` for registration, `patients:write` for patient creation,
`orders:write` for drafts, `orders:read` for order reads, and `orders:sign` for signing and submission.
Existing draft-writing keys do not automatically receive signing permission.

## Register staff and clinicians

You can skip separate clinician registration by supplying `prescriber: { npi }` on order creation or signing.
First-use registration requires `team:write`. Use the explicit registration endpoint below when you want to assign your own external ID.

Send `POST /v1/practices/{practiceId}/users` with an `Idempotency-Key`:

```json
{
  "externalId": "clinician-123",
  "email": "clinician@example.test",
  "name": "Test Prescriber",
  "role": "prescriber",
  "npi": "1234567893",
  "identityAttestation": true
}
```

The response `id` is the `userId` used for orders and signing.
Roles are `administrator`, `prescriber`, `clinical_staff`, `billing`, or `developer`. Registration does not grant ownership.
Existing memberships are preserved; use the member endpoints to change access.

Test registrations require synthetic `.test` email addresses and an Affinity Test NPI.
They create isolated account records and do not claim a real login by email.
Live registrations require approved integration and practice access.
Supply clinician contact details when the clinician has no existing profile. State-license records are optional.
Registration does not verify a login email or clinical credentials; the practice or integrating platform owns credential verification.
Use the [Team endpoints](/guides/provider-access/) to inspect and maintain access.

## Create an order

To prefill prescriptions, use [Prescription defaults and previews](/guides/prescribing-defaults/).
`catalog.prescribingOptions.get` supplies form options. `orders.preview` resolves defaults and returns a creation payload when complete.
A complete preview means draft preparation succeeded. Signing still checks current authority and Live eligibility.
Preview accepts an existing patient ID, an integration patient external ID, or inline patient details. Inline preview does not create a patient.
Explicit overrides remain explicit; preview does not recalculate a manually supplied days supply.
If your EMR already supplies all required values, you can call `orders.create` directly.

Send `POST /v1/orders` with `practiceId`, an optional registered `userId`, and 1–20 complete `prescriptions`.
Each order belongs to one patient. Supply exactly one of:

- `patientId` for an existing patient.
- `patient` containing the same fields as Create patient, without `practiceId`.

For example, the inline patient portion is:

```json
{
  "patient": {
"name": { "first": "Synthetic", "last": "Patient" },
"dateOfBirth": "1980-01-10",
"email": "patient@example.test",
"phone": "+12025550199",
"externalId": "patient-456",
"address": {
  "line1": "100 Test St",
  "city": "Austin",
  "state": "TX",
  "postalCode": "78701",
  "country": "US"
}
  }
}
```

Affinity resolves a matching external identity within the practice and mode, or creates a patient.
Email alone never merges patients. Matching an existing patient preserves their demographics;
use Update patient for corrections. Without an external identity, a new request can create a new patient.
Retry the same request with the same idempotency key to avoid duplication after an uncertain response.

Patient and order creation commit together. If the order fails, its newly created patient is rolled back.
Inline patient creation additionally requires `patients:write`.

Use `POST /v1/order-batches` to create 1–20 patient orders for one practice atomically.
No patient may appear twice, including when two external identities resolve to the same person.

Draft creation does not sign or send prescriptions.
Before signing, use List shipping options for each catalog item and the patient's destination state.
Set `prescriptions[].dispensing.shippingOptionId` to the selected public shipping-option ID.
Set `prescriptions[].dispensing.shippingDestinationType` to `patient`.
Submission uses that signed choice; changing delivery requires a new unsigned version and attestation.
Complete patient allergy review through the allergy endpoints before creating the draft that the clinician will sign.
If allergy history changes after draft creation, cancel the unsigned draft and create a replacement after the allergy review. Review the replacement before signing.
Use `not_reviewed`, `no_known`, and `recorded` accurately; missing history is not an assertion of no allergies.

## Sign from your backend

Show the clinician the current order from `GET /v1/orders/{orderId}`.
Each prescription includes its version, patient and prescriber snapshots, clinical details,
directions, and dispensing choices. Your application authenticates the clinician and collects
their explicit attestation to the complete order.

The order includes an opaque `revision`. Send that exact value as `expectedRevision`.
Do not calculate it or retrieve a newer revision automatically when signing.

Send `POST /v1/orders/{orderId}/sign-and-submit` with an `Idempotency-Key` header and:

```json
{
  "practiceId": "prac_...",
  "prescriber": { "npi": "1234567893" },
  "signatureAttestation": true,
  "expectedRevision": "rev_<value returned with the reviewed order>"
}
```

Use the actual public IDs and revision returned by the API. The revision covers every prescription
in the order. A changed prescription or a change to the prescription set makes an older revision stale.
Existing integrations may send `expectedVersions` containing every prescription ID and version instead.
Supply exactly one of `expectedRevision` or `expectedVersions`.
Use the same revision contract when adding or editing a draft prescription or rejecting an order.
Use the clinician's NPI, Affinity provider ID, or integration-scoped external ID in `prescriber`.
Omit `prescriber` when the draft already has one. An unassigned draft requires a selector at signing.
Actor headers are optional audit metadata. Legacy `userId` requests still require matching clinician actor headers.
First-use NPI registration requires `team:write`. Affinity reuses the registered practice prescriber or creates a non-login identity from NPPES.
Test mode uses Affinity Test NPIs without calling NPPES. `1234567893` is a Test fixture, not a Live example.
An NPI does not authenticate the clinician, restore revoked access, or overwrite an existing profile.
Supply `prescriber.profile.phone` or `prescriber.profile.email` for first-use contact details when needed.
The platform is responsible for authenticating the clinician and obtaining their signing intent.
Affinity records the integration, key, clinician, and exact versions; it does not independently observe the clinician's action.

The server checks current membership, assigned provider, patient readiness, and prescribing authority.
Live additionally requires an active prescriber account connection, organization approval, and applicable clinical and billing controls. Optional license records do not gate signing.
An attributed order can be signed only by its provider. An unassigned order is attributed at signing.
Controlled substances remain unsupported.

Platform-managed orders use the platform's billing account, including orders signed in Clinic.
Configure Live payments in Platform before submission. See [Billing and payments](/guides/billing/).

A `409` reports a conflict, such as a stale revision, changed prescriber details, incomplete patient readiness, or a billing or access block.
Read the current order and the error detail, then resolve the conflict.
Collect a new attestation if the reviewed prescription versions changed. Use a new idempotency key for the corrected request.
Unmet pharmacy-specific clinical requirements return `422 clinical_requirements_unmet` with field-level issues.
Correct the affected fields and have the clinician review the new version before signing.
Do not automatically attest to refreshed versions.

Use `POST /v1/orders/{orderId}/rejection` with the same identity and revision checks plus a `reason` to reject an unsigned order permanently.

## Submit and track

For one backend call after clinician approval, use `orders.signAndSubmit(orderId, params, options)` or
`POST /v1/orders/{orderId}/sign-and-submit`. Supply the same body as signing and an idempotency key.
It signs the complete batch, then attempts each prescription submission. Signing remains recorded if submission fails.

The response status is `submitted`, `partially_submitted`, or `not_submitted`.
Each prescription includes its submission status, fulfillment order ID, or error. A `202` response can contain failed prescriptions; inspect the results.
Replay the same request and key after an uncertain response. A replay returns the original result, including reported failures.
After resolving a submission failure, call `orders.submit` with a new key. Do not sign again unless the prescriptions changed.
Changed prescriptions require renewed clinician review and attestation to the new versions.

The [server-side EMR example](https://github.com/affinity-health/affinity-typescript/blob/main/examples/emr-order.ts)
covers patient resolution, preview overrides, draft creation, approval, retry handling, and verified webhook processing.

For a two-call workflow, first use `POST /v1/orders/{orderId}/sign`, then send
`POST /v1/orders/{orderId}/submit` with `practiceId` and a new `Idempotency-Key`.
The submit call inherits the signed order's prescriber. A successful `sign-and-submit` response
already attempted submission; call `submit` afterward only for a reported submission failure.

Submission rechecks authorization, signature integrity, selected shipping, and fulfillment readiness.
A successful response means submission was queued, not that the pharmacy accepted it.
Follow order reads and [webhooks](/guides/webhooks/) for pharmacy acceptance, failures, and tracking.

Submission can partially succeed across prescriptions. After resolving a failure, retry with a new
idempotency key. Already queued prescriptions are not submitted twice.

## Cancel an order

`POST /v1/orders/{orderId}/cancel` returns the order with a top-level `cancellation` summary.
HTTP `200` means the cancellation request was handled. Check `cancellation.status`:

| Status      | Meaning                                                                   |
| ----------- | ------------------------------------------------------------------------- |
| `confirmed` | The entire order is cancelled.                                            |
| `pending`   | A pharmacy response is still required.                                    |
| `partial`   | Some cancellations failed while others completed or remain pending.       |
| `failed`    | Cancellation did not succeed. The order can continue through fulfillment. |

`cancellation.outcomes` identifies each fulfillment request and its status.
Read `fulfillments[].cancellations[]` for error codes, reasons, and response times.
A draft can be cancelled locally before transmission. After submission, wait for the pharmacy's
confirmation. An accepted cancellation request does not guarantee cancellation.

Follow `cancellation.confirmed`, `cancellation.rejected`, `cancellation.failed`, and
`cancellation.too_late` webhooks. On failure or rejection, retrieve the order and review the next
step; do not assume that fulfillment stopped or submit a replacement automatically.

## Keep your identifiers

Store your reference in `externalOrderId` and each prescription reference in `externalPrescriptionId`.
These identifiers are immutable, case-sensitive, and scoped to their owning integration and mode.
Use `GET /v1/orders?externalOrderId=...` for exact lookup.

Optional `metadata` supports up to 20 scalar fields. It is display and audit context,
not authorization, pharmacy routing, or prescribing instructions.

## Patient demographic validation

Patient create and update accept dates of birth within the past 120 years, with no future dates.
Use USPS state or territory codes and a five-digit ZIP code or ZIP+4.
The API validates ZIP format; it does not verify ZIP-to-state correspondence or deliverability.
Pharmacy eligibility is checked separately against the shipping state.

Phone is optional when creating a patient. When provided, use E.164 format, such as `+12025550199`.
Order preview requires a patient phone number for pharmacies whose submission contract requires it,
including the Test pharmacy. Add it before previewing or submitting an order.

Source: https://docs-staging.affinityrx.com/guides/choose-an-integration/index.mdx
