Compounding pharmacy routing API integration guide

An Affinity pharmacy API integration has five steps: prepare an unsigned order, show the exact prescriptions to the clinician, record their signing decision, submit the signed order, and follow pharmacy progress. A successful HTTP request does not establish that a pharmacy accepted or shipped a prescription.

This walkthrough uses Affinity's documented API contract dated September 28, 2026 and the TypeScript SDK examples. It explains the request sequence and recovery decisions. Affinity publishes this guide and supplies the product described.

Start with an isolated Test workflow

Create a Test API key in Affinity for Platforms. Keep it on your server and confirm its mode with the first-request guide. Use synthetic patients and Affinity Test prescriber identities.

Give the key the permissions its workflow needs. Creating drafts uses orders:write; reading the order uses orders:read; signing requires orders:sign. Inline patient creation also requires patients:write. First-use prescriber registration requires team:write. Draft-writing permission alone does not authorize signing.

Test mode simulates fulfillment. It does not establish that a real pharmacy will accept an order. Live platform access requires Affinity approval, practice Live access, and the applicable patient, prescriber, product, billing, and fulfillment checks. See Test and Live mode.

Resolve the patient and selected offer

Keep your own patient reference in externalId. Affinity resolves that identity within the authorized integration, practice, and mode. Email alone does not merge patients, and finding an existing patient does not overwrite their demographics.

Select an individual catalog offer, including its exact formulation, strength, dosage form, quantity, and eligible shipping choice. A medication group helps discovery; it does not replace selection of the offer the clinician will prescribe.

Use prescribing options and previews to prepare the payload. A complete preview means the draft can be prepared. Signing still checks current authority and eligibility. Complete patient allergy review before creating the draft for signing; an empty history is not an assertion of no known allergies.

Create the unsigned order

An order belongs to one patient and one practice and contains 1–20 complete prescriptions. Creating it does not sign or send them.

The following snippets use the TypeScript SDK. draft is the complete payload prepared from the selected offers and prescribing options. job is your saved workflow record. Generate and persist a different idempotency key for each action before its first request.

import { Affinity } from "@affinity-health/sdk";

const api = new Affinity(process.env.AFFINITY_API_KEY!);
const practice = api.forPractice(practiceId);

const order = await practice.orders.create(
  { patientId, prescriptions: draft.prescriptions },
  { idempotencyKey: job.createOrderKey },
);

Save the returned order ID with your workflow. If the connection fails before you receive the response, repeat the same request with the same key. Generating a new key turns that retry into a new operation.

For a complete application example, see Affinity's public server-side EMR example. It includes patient resolution, preview overrides, clinician approval, and recovery handling.

Bind the signature to what the clinician reviewed

Retrieve the order and display its prescriptions, patient, prescriber, directions, and dispensing choices. Your application authenticates the clinician and obtains their explicit signing intent. Save that decision with the order's opaque revision.

review below represents that saved decision. It must come from the clinician's review, not from an automatic default.

await practice.orders.sign(
  order.id,
  {
    prescriber: { id: review.prescriberId },
    expectedRevision: review.orderRevision,
    signatureAttestation: review.signatureAttestation,
  },
  { idempotencyKey: job.signOrderKey },
);

The revision covers the complete prescription set. If a prescription changes after review, the old revision is stale. Read the conflict, show the changed order, and obtain a new attestation. Do not silently fetch a newer revision and sign it with the earlier consent.

A prescriber may also be selected by NPI. NPI identifies a clinician; it does not authenticate them or grant signing authority. State-license records are optional for Affinity's non-controlled workflow. Controlled substances are unsupported. The headless integration guide defines identity and authorization requirements.

Treat submission as a separate outcome

After signing, submit with a different saved key:

const submission = await practice.orders.submit(order.id, {
  idempotencyKey: job.submitOrderKey,
});

The submit endpoint returns an overall acknowledgement or an error. Retrieve the order and follow its events to inspect fulfillment progress. A submission acknowledgement means the send was queued; it does not establish pharmacy acceptance.

Affinity also supports POST /v1/orders/{orderId}/sign-and-submit when the clinician has already reviewed and approved the order. It accepts the reviewed revision and explicit attestation, signs the prescriptions, and attempts submission. Its overall result can be submitted, partially_submitted, or not_submitted. An HTTP 202 can include failed prescriptions.

Signing remains recorded if submission fails. After resolving a reported submission failure, use the submit operation with a new key. Already queued prescriptions are not sent twice. Do not sign the order again unless its prescriptions changed and the clinician reviewed the new versions.

Distinguish an unknown result from a reported failure

The recovery decision depends on what you know:

  • The connection timed out. Replay the same action with the same body and key to recover its original result. Do not assume the server did nothing.
  • The response reports a failed submission. Resolve the reported cause, then make a new submission attempt with a new key. Replaying the old key returns its original result, including its failure.
  • The reviewed revision is stale. Obtain a fresh clinician review before another signing attempt. A transport retry cannot supply new clinical consent.
  • The pharmacy has not acknowledged the order. Read its current state and investigate. Silence does not establish rejection or permission to send a replacement elsewhere.

These cases require different actions. A blanket retry loop with a new key on every attempt can create duplicates; a loop that always reuses the old key cannot advance a corrected request.

Verify and save webhooks before acknowledging them

Verify the unmodified request body with the SDK before using its contents:

import { verifyAffinityWebhook } from "@affinity-health/sdk";

const event = await verifyAffinityWebhook({
  body: await request.arrayBuffer(),
  secret: process.env.AFFINITY_WEBHOOK_SECRET!,
  signature: request.headers.get("affinity-signature"),
});

Then save the event durably and deduplicate by event ID before returning 2xx. Process downstream work asynchronously. If verification or durable storage fails, do not acknowledge successful receipt.

Events can arrive more than once and out of order. In the background job, retrieve the current order in the same organization and mode before updating displayed state. An older snapshot must not overwrite a newer state.

Subscribe to the documented events your application handles, such as order.signed, order.submitted, order.accepted, order.shipped, and order.delivered. The event name and the order status are different fields. The webhook guide documents their meanings, signature verification, retries, and replay.

Confirm cancellation before sending a replacement

An HTTP 200 from the cancel endpoint means the cancellation request was handled. Inspect cancellation.status: it can be confirmed, pending, partial, or failed.

After submission, a pharmacy response may still be required. Follow cancellation events and retrieve the order. Do not tell a patient that fulfillment stopped, or automatically create a replacement, while cancellation is unresolved. Our pharmacy failover guide explains why uncertain ownership creates duplicate risk.

A concrete acceptance checklist

Before enabling Live traffic, run these scenarios against your Test integration and retain the request IDs and outcomes without patient data or credentials:

  1. Create one draft twice with the same saved key and verify that the returned order identity is unchanged.
  2. Change a prescription after review and verify that signing with the earlier revision fails.
  3. Inspect every prescription result in a partial submission, then recover the failed submission without repeating successful sends.
  4. Deliver the same webhook twice and verify that your application applies its effect once.
  5. Process an older event after a newer one and verify that the displayed order retains its current state.
  6. Send an invalid webhook signature and verify that no event is accepted or applied.
  7. Request cancellation and keep the order pending until the response or event confirms the actual outcome.

Test success establishes behavior in the simulator. Live readiness also depends on the approved pharmacies, products, destinations, and operational workflow.

Sources and related guides

This article was checked against Affinity's headless integration guide, TypeScript SDK guide, webhook contract, and API reference on September 30, 2026. Those references own the current request and response schemas.