Skip to content

Orders lifecycle ​

Summary: call POST /orders/check with the order body before you take payment, take payment only when it answers wouldAccept: true, then send the same body to POST /orders. Follow the order with webhooks (or GET /orders/{id}) from received to delivered. You can edit it while it is received (unless it was split across pharmacies) and cancel it until it has been sent to a pharmacy.

The whole flow ​

Loading diagram...

1. Check before you take payment ​

POST /api/inbound/{slug}/orders/check takes exactly the body you would send to create the order and returns every reason it would be refused, for the order and for each line, using the same codes create uses. It creates, charges and reserves nothing, and needs no Idempotency-Key.

A check that would be refused, from sandbox (the prescriber's phone and practice address were left out):

json
{
  "wouldAccept": false,
  "eligible": true,
  "checkedAt": "2026-10-25T17:57:57.512Z",
  "order": [
    {
      "code": "prescriber_incomplete",
      "blockers": [
        { "code": "prescriber_address_missing", "field": "physician.address", "message": "Prescriber practice address is required by the fulfilling pharmacy. ..." },
        { "code": "prescriber_phone_missing", "field": "physician.phone", "message": "Prescriber phone is required by the fulfilling pharmacy. ..." }
      ]
    }
  ],
  "lines": [{ "sku": "EXAMPLE-SKU-1", "eligible": true, "unitPriceCents": 17000, "reasons": [] }],
  "estimate": { "goodsCents": 17000, "prepaidAppliedCents": 0, "couponCents": 0, "shippingCents": 1000, "totalCents": 18000 },
  "notChecked": ["idempotency_key", "credit_cap", "exposure_cap"],
  "warnings": []
}

A line-level refusal looks like this: { "sku": "NOT-A-REAL-SKU", "eligible": false, "unitPriceCents": null, "reasons": [{ "code": "unknown_product", "sku": "NOT-A-REAL-SKU", "reason": "unrecognised" }] }.

  • wouldAccept is the answer to act on: true means create would accept this body right now.
  • eligible ignores missing order details (the prescriber_incomplete and order_incomplete reasons) and looks only at whether the products, clinic and account can be ordered. Use it to tell "fix the form" apart from "this cannot be ordered".
  • order lists order-level reasons, each { "code": "...", ...details }. lines gives each SKU you sent with its own reasons and your unit price.
  • estimate is what the order would charge, in cents.
  • notChecked names what a check did not test. It always includes idempotency_key; in sandbox it also includes credit_cap and exposure_cap, because sandbox applies no credit limits.
  • A check is a snapshot. Stock, an overdue invoice or a credit limit can change between the check and the create, and create checks everything again, so create's answer wins.

Accounts on API version 2 are certified on this before going live: your checkout must not reach payment for an order the check refuses. See the Go-live checklist.

2. Create the order ​

POST /api/inbound/{slug}/orders with an Idempotency-Key header. The body's idempotency_key must be the same value as the header, or the request is refused with 400 idempotency_key_mismatch and nothing is created. Use one new key per logical order and reuse it for every retry of that order. See Idempotency.

What the body needs, in short (every field and limit is in the reference):

PartRequiredNotes
partner_order_idyesYour id for the order, up to 128 characters, and not starting with AXM-. It must be unique among your live orders; once every earlier order with that id is failed or cancelled, you may use it again.
idempotency_keyyesSame value as the Idempotency-Key header.
patientyesfirst_name, last_name, dob (YYYY-MM-DD, a real date, patient aged 13 to 120), sex (M, F or X), address (US address with a two-letter state code and a 5 or 9 digit ZIP), and phone (see below).
physicianyesname and a 10-digit npi, plus the structured fields the pharmacy needs to fill: first_name, last_name, credential, phone and practice address.
itemsyes1 to 50 lines, each with sku, strength, quantity (1 to 10000), directions and optional refills (0 to 99).
clinicusuallyexternal_id (your stable id for the clinic) identifies who ordered. See Clinics and agreements.
shipping_methodrecommendedstandard (3 to 5 business days, $10) or overnight (cold pack, next business day, $25). If you leave it out, the order ships and bills as overnight.
external_notesnoUp to 2000 characters of your own notes. Stored with the order and returned as externalNotes. Not sent to the pharmacy: anything the pharmacy must act on belongs in items[].directions, or in a support ticket.
metadatanoYour own key-values, up to 8000 characters of serialized JSON. Stored with the order; not returned in the order.

Unknown top-level fields are ignored, not refused, so check spelling: a misspelled optional field is silently dropped.

A successful create returns 201 with axiomOrderId, partnerOrderId, the full order, and two arrays:

  • warnings: empty on an accepted order, except for advance notices of a requirement that has a start date (today, patient_phone_required_soon, below).
  • advisories: suggestions that never block the order, for example injector_pen_missing (a cartridge ordered without an injector pen).

Required order details (the 422 you will meet most) ​

If the order is missing details the pharmacy needs to fill it, create refuses it with 422. The error is prescriber_incomplete when every gap is a prescriber field, otherwise order_incomplete, and blockers names each field. Sandbox applies exactly the same rule.

json
{
  "error": "prescriber_incomplete",
  "message": "The order is missing details required to fill it. Resubmit with the listed fields so the order can be routed and filled.",
  "blockers": [
    { "code": "prescriber_phone_undialable", "field": "physician.phone", "message": "..." }
  ]
}
Blocker codeFieldFix
patient_sex_missingpatient.sexSend M, F or X.
patient_phone_missingpatient.phoneRequired from 2026-11-20 (below).
patient_phone_undialablepatient.phoneRequired from 2026-11-20. Send ten digits, 406-555-0142, or E.164 +14065550142, with no extension.
prescriber_npi_invalidphysician.npiTen digits that fail the NPI check digit are not a real NPI.
prescriber_address_missingphysician.addressSend the practice address.
prescriber_phone_missingphysician.phoneSend the prescriber or practice phone.
prescriber_phone_undialablephysician.phoneTen digits, 555-123-4567, or E.164 +15551234567.
prescriber_first_name_missingphysician.first_nameCould not be taken from name. Send it.
prescriber_last_name_missingphysician.last_nameCould not be taken from name. Send it.
prescriber_credential_missingphysician.credentialSend the credential, for example MD, DO, NP or PA.
prescriber_credential_unsupportedphysician.credentialThe credential is not one the pharmacy recognises.

When the structured prescriber fields are absent we try to read them from name ("Dr. Alex Example, MD" or "Example, Alex"). Sending them explicitly is more reliable.

The patient phone becomes required on 2026-11-20 at 00:00 US Eastern. Until then an order with a missing or undialable patient.phone is accepted and carries a patient_phone_required_soon entry in warnings (and in the check's warnings). From that moment the same order is refused with 422 order_incomplete.

Other reasons an order is refused ​

Each refusal is { "error": "<code>", ...details }. The check endpoint reports the same codes without creating anything.

StatusCodeMeans
400validation_failedThe body failed schema validation; issues lists each problem.
400idempotency_key_mismatchHeader and body keys differ.
409partner_order_id_duplicateYou already have a live order with this partner_order_id. The body carries partnerOrderId and axiomOrderId. A retry with the original Idempotency-Key returns the original 201 instead.
409idempotency_key_reusedThis key already created a different order.
409order_in_flightThe same order is being created by a concurrent request: retry shortly with the same key. If it persists, the key and partner_order_id belong to an earlier failed or cancelled order: read it and follow section 5 below before resubmitting under a new key.
422unknown_productThe SKU is not one we recognise for you. reason says why: product_inactive, alias_inactive, alias_not_for_this_partner or unrecognised.
422product_retiredThe SKU was retired for good. Carries sku and retiredAt. Nothing is substituted.
422product_not_in_catalogThe SKU exists but is not in your catalog, or not in this clinic's.
422sku_not_pricedNo price is set up for you on this SKU. Contact us.
422product_not_yet_availableHeld until availableAt (it can still be backordered after that).
422product_backorderedOut of stock; expectedBackAt when known.
422ship_state_not_licensedNo pharmacy for this order is licensed to ship to state.
422product_unavailable_in_stateThis SKU cannot ship to state.
422no_pharmacy_can_fillNo pharmacy available to this order can fill sku for state today.
422agreement_unsignedThe clinic has no signed agreement. Read reason; see Clinics and agreements.
422clinic_overdue / partner_overdueAn invoice is past due. Ordering resumes once it is paid. See Billing.
422clinic_suspendedThe clinic is suspended. Contact us.
422prescriber_inactiveThe prescriber was removed from the clinic's provider list. This check is being introduced; we will announce when it is switched on.
422clinic_over_credit_capThe order would take the clinic over its credit limit. Carries capCents, outstandingCents, orderChargeCents and projectedCents.
422partner_exposure_capThe order would take your account over its overall limit.
422billing_account_id_unsupportedbilling.account_id was sent without clinic.external_id, or with a different value. Send the same value in both, or leave billing.account_id out.

The full list, including rate limits and server errors, is in Errors and rate limits.

3. Follow the order ​

Statuses ​

StatusMeaning
receivedAccepted, not yet with a pharmacy. You can usually still edit it (not once it has been split across pharmacies).
routedAssigned to a licensed pharmacy.
in_productionThe pharmacy is preparing it.
ready_to_packPrepared, waiting to be packed.
ready_to_shipPacked, waiting for the carrier.
shippedOn its way. shipments lists the tracking references we hold (tracking number and carrier). trackingNumber and carrier on the order describe its own box and stay null for an order split across pharmacies, so read shipments. A split order can read received while some of its boxes are on their way, and reads shipped once its parts are settled: shipped, or failed or cancelled and credited. So shipped on a split order does not mean everything shipped: shipments and the billing feed show what did.
deliveredThe carrier reports it delivered.
cancelledCancelled by you or by Axiom.
failedIt was not filled as submitted. Read failure before doing anything else (below): it is not always safe to resubmit.

Orders do not always pass through every status: a pharmacy may report shipped without the steps before it.

Webhooks ​

order.status_changed is sent for routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered and cancelled, and again when a shipped order's tracking changes. It carries oldStatus, newStatus, the fulfilling pharmacy, trackingNumber, carrier, shipments, shippedAt and deliveredAt. See Webhooks.

Reading orders ​

  • GET /orders/{partnerOrderId} returns the order and its events timeline (each with status, source, createdAt and a small payload such as the tracking number). You can also pass the numeric axiomOrderId. An order that is not yours, or not in this key's environment, is 404 not_found.
  • GET /orders lists your orders, newest first. Filter with status and since (orders created at or after that time), and page with limit (up to 500, default 100) and offset. Filtering on shipped includes orders that have shipped in part.

Prefer webhooks for status. To reconcile, re-read by id every order from your reconciliation window (for example, created in the last 60 days), whatever its status: a failed order can go back to routed, a pharmacy can resume a cancelled one, and Axiom can correct a delivered one: since filters on creation time, so the list alone does not show you which older orders changed.

4. Edit or cancel ​

Edit with PATCH /orders/{partnerOrderId} while the order is received. Only the patient's name and phone and the shipping address can change; anything else in the body is refused with 400. A new ship-to state is checked again for licensing and availability. After the order is routed, or once it has been split across pharmacies (even while it still reads received), edit returns 409 order_not_editable.

Cancel with POST /orders/{partnerOrderId}/cancel and an optional reason (up to 500 characters). You can cancel while the order is received or routed and has not been sent to the pharmacy. After that, cancel returns 409 order_not_cancellable with a reason such as dispatched, and the pharmacy has to be contacted: open a support ticket.

Both endpoints need an Idempotency-Key, scoped to the order in the path.

5. When an order cannot be filled ​

An order can end up failed in more than one way, and not every way sends you an event:

  • When we cannot route it or cannot send it to the pharmacy, you get an order.failed webhook with a code (no_eligible_pharmacy, hard_rule_conflict, routing_failed or pharmacy_dispatch_failed), a plain-language message, and retryable: false.
  • When the pharmacy reports an order failed, there is no order.failed and no order.status_changed for it. You see it only by reading the order, which is one more reason to re-read your recent orders regularly, whatever their status.
  • When the order's status is failed and its last dispatch attempt failed, GET /orders/{id} includes failure: { code, detail }.

Read failure.code before you resubmit anything.

failure.codeMeansDo
A field code, for example missing_prescriber_phone or unsupported_prescriber_credentialThe order lacked something the pharmacy needs. detail names it.Fix it and submit a new order.
pharmacy_rejectedThe pharmacy's system rejected it.Ask Axiom support for the reason before resubmitting.
pharmacy_outcome_unknownThe pharmacy did not confirm whether it accepted the order, so it may already hold the prescription.Do not resubmit. Axiom is reconciling with the pharmacy.
dispatch_failedThe order could not be sent.Axiom is reviewing it; wait for an update or ask support.
No failureThere is no dispatch error to show: it may have failed before reaching a pharmacy (for example no_eligible_pharmacy) or been failed by the pharmacy after it accepted the order.Ask support before resubmitting.

A failed order is never resubmitted automatically. When the table above says it is safe to submit a corrected order, you may reuse the same partner_order_id (the earlier order is failed or cancelled), but always with a new Idempotency-Key: the old key stays attached to the old order. Reusing it either replays the old order's stored response (identical bytes, within 7 days) or is refused (409 idempotency_key_reused or 409 order_in_flight).

Re-ships and split orders ​

  • An order can ship in several boxes. shipments lists each tracking reference we hold with its axiomOrderId, trackingNumber and carrier. It is a list of references, not a confirmed count of boxes: a reference can stay after a shipment is corrected.
  • If Axiom re-ships an order, the two orders point at each other: reship.replaces on the new one and reship.replacedBy on the old one, each with axiomOrderId and partnerOrderId.