Skip to content

Build a headless integration

Register clinicians, prepare orders, and sign and submit from your backend.

Updated View as Markdown

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

{
  "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 to inspect and maintain access.

Create an order

To prefill prescriptions, use Prescription defaults and previews. 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:

{
  "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:

{
  "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.

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

Navigation

Type to search…

↑↓ navigate↵ selectEsc close