Appearance
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" }] }.
wouldAcceptis the answer to act on:truemeans create would accept this body right now.eligibleignores missing order details (theprescriber_incompleteandorder_incompletereasons) 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".orderlists order-level reasons, each{ "code": "...", ...details }.linesgives each SKU you sent with its ownreasonsand your unit price.estimateis what the order would charge, in cents.notCheckednames what a check did not test. It always includesidempotency_key; in sandbox it also includescredit_capandexposure_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):
| Part | Required | Notes |
|---|---|---|
partner_order_id | yes | Your 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_key | yes | Same value as the Idempotency-Key header. |
patient | yes | first_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). |
physician | yes | name and a 10-digit npi, plus the structured fields the pharmacy needs to fill: first_name, last_name, credential, phone and practice address. |
items | yes | 1 to 50 lines, each with sku, strength, quantity (1 to 10000), directions and optional refills (0 to 99). |
clinic | usually | external_id (your stable id for the clinic) identifies who ordered. See Clinics and agreements. |
shipping_method | recommended | standard (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_notes | no | Up 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. |
metadata | no | Your 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 exampleinjector_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 code | Field | Fix |
|---|---|---|
patient_sex_missing | patient.sex | Send M, F or X. |
patient_phone_missing | patient.phone | Required from 2026-11-20 (below). |
patient_phone_undialable | patient.phone | Required from 2026-11-20. Send ten digits, 406-555-0142, or E.164 +14065550142, with no extension. |
prescriber_npi_invalid | physician.npi | Ten digits that fail the NPI check digit are not a real NPI. |
prescriber_address_missing | physician.address | Send the practice address. |
prescriber_phone_missing | physician.phone | Send the prescriber or practice phone. |
prescriber_phone_undialable | physician.phone | Ten digits, 555-123-4567, or E.164 +15551234567. |
prescriber_first_name_missing | physician.first_name | Could not be taken from name. Send it. |
prescriber_last_name_missing | physician.last_name | Could not be taken from name. Send it. |
prescriber_credential_missing | physician.credential | Send the credential, for example MD, DO, NP or PA. |
prescriber_credential_unsupported | physician.credential | The 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.
| Status | Code | Means |
|---|---|---|
| 400 | validation_failed | The body failed schema validation; issues lists each problem. |
| 400 | idempotency_key_mismatch | Header and body keys differ. |
| 409 | partner_order_id_duplicate | You 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. |
| 409 | idempotency_key_reused | This key already created a different order. |
| 409 | order_in_flight | The 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. |
| 422 | unknown_product | The SKU is not one we recognise for you. reason says why: product_inactive, alias_inactive, alias_not_for_this_partner or unrecognised. |
| 422 | product_retired | The SKU was retired for good. Carries sku and retiredAt. Nothing is substituted. |
| 422 | product_not_in_catalog | The SKU exists but is not in your catalog, or not in this clinic's. |
| 422 | sku_not_priced | No price is set up for you on this SKU. Contact us. |
| 422 | product_not_yet_available | Held until availableAt (it can still be backordered after that). |
| 422 | product_backordered | Out of stock; expectedBackAt when known. |
| 422 | ship_state_not_licensed | No pharmacy for this order is licensed to ship to state. |
| 422 | product_unavailable_in_state | This SKU cannot ship to state. |
| 422 | no_pharmacy_can_fill | No pharmacy available to this order can fill sku for state today. |
| 422 | agreement_unsigned | The clinic has no signed agreement. Read reason; see Clinics and agreements. |
| 422 | clinic_overdue / partner_overdue | An invoice is past due. Ordering resumes once it is paid. See Billing. |
| 422 | clinic_suspended | The clinic is suspended. Contact us. |
| 422 | prescriber_inactive | The prescriber was removed from the clinic's provider list. This check is being introduced; we will announce when it is switched on. |
| 422 | clinic_over_credit_cap | The order would take the clinic over its credit limit. Carries capCents, outstandingCents, orderChargeCents and projectedCents. |
| 422 | partner_exposure_cap | The order would take your account over its overall limit. |
| 422 | billing_account_id_unsupported | billing.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
| Status | Meaning |
|---|---|
received | Accepted, not yet with a pharmacy. You can usually still edit it (not once it has been split across pharmacies). |
routed | Assigned to a licensed pharmacy. |
in_production | The pharmacy is preparing it. |
ready_to_pack | Prepared, waiting to be packed. |
ready_to_ship | Packed, waiting for the carrier. |
shipped | On 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. |
delivered | The carrier reports it delivered. |
cancelled | Cancelled by you or by Axiom. |
failed | It 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 itseventstimeline (each withstatus,source,createdAtand a smallpayloadsuch as the tracking number). You can also pass the numericaxiomOrderId. An order that is not yours, or not in this key's environment, is404 not_found.GET /orderslists your orders, newest first. Filter withstatusandsince(orders created at or after that time), and page withlimit(up to 500, default 100) andoffset. Filtering onshippedincludes 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.failedwebhook with acode(no_eligible_pharmacy,hard_rule_conflict,routing_failedorpharmacy_dispatch_failed), a plain-languagemessage, andretryable: false. - When the pharmacy reports an order failed, there is no
order.failedand noorder.status_changedfor 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
failedand its last dispatch attempt failed,GET /orders/{id}includesfailure: { code, detail }.
Read failure.code before you resubmit anything.
failure.code | Means | Do |
|---|---|---|
A field code, for example missing_prescriber_phone or unsupported_prescriber_credential | The order lacked something the pharmacy needs. detail names it. | Fix it and submit a new order. |
pharmacy_rejected | The pharmacy's system rejected it. | Ask Axiom support for the reason before resubmitting. |
pharmacy_outcome_unknown | The 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_failed | The order could not be sent. | Axiom is reviewing it; wait for an update or ask support. |
No failure | There 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.
shipmentslists each tracking reference we hold with itsaxiomOrderId,trackingNumberandcarrier. 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.replaceson the new one andreship.replacedByon the old one, each withaxiomOrderIdandpartnerOrderId.

