Appearance
2. Orders
Check an order before you take payment, create it, follow it to delivery, and edit or cancel it while that is still possible.
Read first: Orders lifecycle.
Base URL https://api.axiomscript.com. Every request is signed; see Authentication and signing.
POST
/orders/checkCheck an order before taking paymentPOST/ordersCreate an orderGET/orders/{partnerOrderId}Get an order with its event timelineGET/ordersList ordersPATCH/orders/{partnerOrderId}Edit a not-yet-routed orderPOST/orders/{partnerOrderId}/cancelCancel an orderCheck an order before taking payment
POST/api/inbound/{partnerSlug}/orders/check
Send the SAME body you would send to create an order, and get back every reason that order would be refused, per line and for the order as a whole, using exactly the codes order create returns. Nothing is created, charged, drawn or reserved, so call it as often as you like, and always at the moment you take the patient's payment. No Idempotency-Key is needed. A check is a snapshot: stock, overdue invoices and credit can change between the check and the create, and create re-checks everything, so its answer wins. checkedAt lets you decide how old a check you trust. It predicts the outcome of submitting the body as a NEW order; it does not predict an idempotent replay of an earlier create. A line that is unknown, outside your catalogue or not priced for you reports only that, exactly as create would. notChecked names the create gates the check could not evaluate (always idempotency_key, because the header comparison needs the header; in sandbox also the gates sandbox skips).
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
Request body
partnerstringrequiredYour partner slug. Informational only: the request is authenticated and attributed from the path slug plus the HMAC signature, and this field is not cross-checked against them. 1 to 64 characters.
partner_order_idstringrequired1 to 128 characters.
idempotency_keystringrequired1 to 128 characters.
shipping_methodstringoptionalShipping service level, stamped on the order and billed as its own invoice line. standard is $10 (3 to 5 business days), overnight is $25 (cold pack, next business day). OMITTING THIS FIELD BILLS OVERNIGHT ($25), the level every order had before service levels existed. Send standard explicitly if that is the level your clinics chose. One of: standard, overnight.
patientobjectrequired
patient.first_namestringrequired1 to 100 characters.
patient.last_namestringrequired1 to 100 characters.
patient.dobstring (date)requiredCalendar date YYYY-MM-DD.
patient.sexstringrequiredOne of: M, F, X.
patient.phonestringoptionalPatient phone. Optional today; REQUIRED and must be dialable from 2026-11-20 (see PATIENT PHONE above). Until then a missing or undialable phone returns a patient_phone_required_soon warning. Up to 32 characters.
patient.addressobjectrequired
patient.address.line1stringrequired1 to 200 characters.
patient.address.line2stringoptionalUp to 200 characters.
patient.address.citystringrequired1 to 100 characters.
patient.address.statestringrequiredTwo-letter US state code. Pattern ^[A-Za-z]{2}$.
patient.address.zipstringrequired5-digit ZIP or ZIP+4. Pattern ^\d{5}(-\d{4})?$.
physicianobjectrequiredPrescriber on the script. Without phone or address the order is refused 422 prescriber_incomplete (not a 400), so they are marked required here. The fulfilling pharmacy also needs structured first/last name, credential, phone, and the prescriber's practice address to fill the order. Send all of them. An order missing any of these is REJECTED at intake with 422 prescriber_incomplete and never created, in BOTH sandbox and production. Sandbox used to accept it with warnings; it no longer does, so a payload that passes this gate in sandbox passes it in production (production still adds credit limits and, on some accounts, the clinic agreement). Patient name/phone and the shipping address can be corrected afterwards with PATCH /orders/{partnerOrderId} while the order is still received and has not been split across pharmacies; prescriber fields are not editable, so a prescriber gap is a cancel + resubmit.
physician.namestringrequired1 to 200 characters.
physician.npistringrequired10-digit NPI. The schema checks the digit count only, but the number is additionally validated against the CMS check digit (Luhn over "80840" plus the first 9 digits). A ten-digit number that fails the check digit is rejected with 422 prescriber_incomplete and a prescriber_npi_invalid blocker, in BOTH environments. The fulfilling pharmacy cannot fill against a malformed NPI. Pattern ^\d{10}$.
physician.deastringoptionalOptional. DEA registration number. Up to 20 characters.
physician.first_namestringoptionalPrescriber first name (preferred over splitting name). Up to 100 characters.
physician.last_namestringoptionalPrescriber last name. Up to 100 characters.
physician.credentialstringoptionale.g. MD, DO, NP, PA. Up to 16 characters.
physician.phonestringrequiredPrescriber / practice phone. Up to 32 characters.
physician.addressobjectrequiredPrescriber's practice address. Required by the pharmacy to fill.
physician.address.line1stringrequired1 to 200 characters.
physician.address.line2stringoptionalUp to 200 characters.
physician.address.citystringrequired1 to 100 characters.
physician.address.statestringrequiredTwo-letter US state code. Pattern ^[A-Za-z]{2}$.
physician.address.zipstringrequired5-digit ZIP or ZIP+4. Pattern ^\d{5}(-\d{4})?$.
itemsarray of objectrequiredAt least 1 item. At most 50 items.
items[].skustringrequired1 to 64 characters.
items[].strengthstringrequired1 to 64 characters.
items[].quantityintegerrequiredFrom 1 to 10000.
items[].directionsstringrequired1 to 1000 characters.
items[].refillsintegeroptionalFrom 0 to 99. Default 0.
billingobjectoptionalInformational. method is recorded on the payload but does not select a payment rail: invoicing terms are set per clinic on our side.
billing.methodstringrequiredOne of: ach_invoice, card, plaid, wire.
billing.account_idstringoptionalYour clinic-account identifier. It must EQUAL clinic.external_id, which is the field clinic attribution actually resolves on. Sending an account_id with no clinic.external_id, or one that disagrees with it, is rejected 422 billing_account_id_unsupported rather than silently ignored: the two would name different billable accounts and there is no precedence between them, so accepting the order would invoice and credit-cap a clinic you did not name. Send the account as clinic.external_id, or omit this field. Up to 128 characters.
clinicobjectoptionalOptional clinic identity. With an external_id, the order goes to your clinic with that id, and a clinic we have never seen is auto-created from this block on its first order (order-first onboarding), so you do not have to call POST /clinics first, EXCEPT on a partner-attested account, where the clinic must be enrolled with its attestation before its first order is accepted. Without an external_id, npi is used when exactly one of your clinics has it, and otherwise the order goes to your account's default clinic when one is configured (with none, the order has no clinic). Send the contact fields so an auto-created clinic has a real name and a reachable contact instead of a blank placeholder.
clinic.external_idstringoptionalYour own id for the clinic. 1 to 128 characters.
clinic.location_idstringoptionalThe SITE inside this clinic account the order came from, for an account that spans several locations. A short code (letters, digits, dot, dash, underscore, starting with a letter or digit), never a description. It does not create or select a clinic: the account is still resolved by external_id / npi. Stored on the order, echoed back as clinicLocation, and carried onto Axiom's reconciliation export so each site can be reconciled. It is not printed on invoices yet. Omit it for the account's default location. Pattern ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$.
clinic.location_namestringoptionalDisplay name for location_id, e.g. the site's town. Rejected (400) without location_id. 1 to 120 characters.
clinic.npistringoptionalPattern ^\d{10}$.
clinic.namestringoptional1 to 200 characters.
clinic.primary_contact_emailstring (email)optionalUp to 255 characters.
clinic.primary_contact_phonestringoptional1 to 32 characters.
clinic.pricing_tierstringoptionalPricing tier assigned to the clinic. Values are agreed with Axiom. Applied ONLY when this order auto-creates the clinic, and ignored for a clinic that already exists. One of: tier_1, tier_2, tier_3, enterprise.
external_notesstringoptionalFree-text notes for this order (special handling, clinic instructions). Stored on the order and echoed back as externalNotes on the order envelope, and visible to Axiom operators. Note these are NOT currently transmitted to the fulfilling pharmacy, so do not put fill-critical instructions here. Up to 2000 characters.
metadataobjectoptionalOpaque passthrough blob, capped at 8000 characters of serialized JSON.
Responses
| Status | Meaning |
|---|---|
200 | The check ran. Read wouldAccept (and eligible), whatever it found. |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (200)
wouldAcceptbooleanrequiredCreate would accept this body now. eligible and the order is complete.
eligiblebooleanrequiredEvery product, clinic and account gate passes. Completeness (prescriber, patient) is reported separately.
checkedAtstring (date-time)required
orderarray of objectrequiredOrder-level reasons (clinic, account, completeness), each in create's code and details.
order[].codestringrequired
linesarray of objectrequired
lines[].skustringrequiredThe SKU as you sent it.
lines[].eligiblebooleanrequired
lines[].unitPriceCentsinteger | nullrequiredWhat create would charge per unit
lines[].reasonsarray of objectrequired
lines[].reasons[].codestringrequired
estimateobject | nullrequiredAn ESTIMATE of what create would charge right now, including prepaid units and the clinic's auto-applied coupon. Null when the check stopped early.
estimate.goodsCentsintegerrequired
estimate.prepaidAppliedCentsintegerrequired
estimate.couponCentsintegerrequired
estimate.shippingCentsintegerrequired
estimate.totalCentsintegerrequired
notCheckedarray of stringrequired
warningsarray of objectrequiredNON-BLOCKING notices, the same entries order create's 201 would carry in its warnings. Never a refusal: they do not affect wouldAccept or eligible. Today the only code is patient_phone_required_soon (see PATIENT PHONE on order create). [] for a compliant order.
warnings[].codestringrequiredStable machine code. One of prescriber_address_missing, prescriber_phone_missing, prescriber_phone_undialable, prescriber_first_name_missing, prescriber_last_name_missing, prescriber_credential_missing, prescriber_credential_unsupported, prescriber_npi_invalid, patient_sex_missing, patient_phone_missing, patient_phone_undialable, and (a non-blocking notice, only in a 201 or a check warnings array) patient_phone_required_soon.
warnings[].fieldstringrequiredDotted path of the field, e.g. physician.address.
warnings[].messagestringrequiredHuman-readable explanation.
Request
http
POST /api/inbound/acme-health/orders/check
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>json
{
"partner": "acme-health",
"partner_order_id": "ACME-0001",
"idempotency_key": "8f41caf8-c8b0-4e57-9e9c-30ec25eec49c",
"shipping_method": "standard",
"patient": {
"first_name": "Jane",
"last_name": "Doe",
"dob": "1980-01-01",
"sex": "F",
"phone": "406-555-0142",
"address": {
"line1": "123 Main St",
"city": "Springfield",
"state": "TX",
"zip": "62701"
}
},
"physician": {
"name": "Dr. Alex Example",
"first_name": "Alex",
"last_name": "Example",
"credential": "MD",
"npi": "1234567893",
"phone": "406-555-0100",
"address": {
"line1": "500 Clinic Way",
"city": "Springfield",
"state": "TX",
"zip": "62701"
}
},
"clinic": {
"external_id": "acme-clinic-001",
"name": "Example Wellness Clinic"
},
"items": [
{
"sku": "EXAMPLE-SKU-1",
"strength": "10mg",
"quantity": 1,
"directions": "Use as directed.",
"refills": 0
}
]
}Response 200
json
{
"wouldAccept": true,
"eligible": true,
"checkedAt": "2026-10-25T18:57:39.139Z",
"order": [],
"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": []
}Create an order
POST/api/inbound/{partnerSlug}/orders
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
Idempotency-KeystringheaderrequiredREQUIRED on every endpoint that creates or changes something (see the Idempotency section of the API description for the list). Omitting it is a 400 idempotency_key_required. Replaying the same key with the SAME body returns the original response. Reusing a key with a DIFFERENT body is a 409 idempotency_key_reused. Max 128 characters.
Request body
partnerstringrequiredYour partner slug. Informational only: the request is authenticated and attributed from the path slug plus the HMAC signature, and this field is not cross-checked against them. 1 to 64 characters.
partner_order_idstringrequired1 to 128 characters.
idempotency_keystringrequired1 to 128 characters.
shipping_methodstringoptionalShipping service level, stamped on the order and billed as its own invoice line. standard is $10 (3 to 5 business days), overnight is $25 (cold pack, next business day). OMITTING THIS FIELD BILLS OVERNIGHT ($25), the level every order had before service levels existed. Send standard explicitly if that is the level your clinics chose. One of: standard, overnight.
patientobjectrequired
patient.first_namestringrequired1 to 100 characters.
patient.last_namestringrequired1 to 100 characters.
patient.dobstring (date)requiredCalendar date YYYY-MM-DD.
patient.sexstringrequiredOne of: M, F, X.
patient.phonestringoptionalPatient phone. Optional today; REQUIRED and must be dialable from 2026-11-20 (see PATIENT PHONE above). Until then a missing or undialable phone returns a patient_phone_required_soon warning. Up to 32 characters.
patient.addressobjectrequired
patient.address.line1stringrequired1 to 200 characters.
patient.address.line2stringoptionalUp to 200 characters.
patient.address.citystringrequired1 to 100 characters.
patient.address.statestringrequiredTwo-letter US state code. Pattern ^[A-Za-z]{2}$.
patient.address.zipstringrequired5-digit ZIP or ZIP+4. Pattern ^\d{5}(-\d{4})?$.
physicianobjectrequiredPrescriber on the script. Without phone or address the order is refused 422 prescriber_incomplete (not a 400), so they are marked required here. The fulfilling pharmacy also needs structured first/last name, credential, phone, and the prescriber's practice address to fill the order. Send all of them. An order missing any of these is REJECTED at intake with 422 prescriber_incomplete and never created, in BOTH sandbox and production. Sandbox used to accept it with warnings; it no longer does, so a payload that passes this gate in sandbox passes it in production (production still adds credit limits and, on some accounts, the clinic agreement). Patient name/phone and the shipping address can be corrected afterwards with PATCH /orders/{partnerOrderId} while the order is still received and has not been split across pharmacies; prescriber fields are not editable, so a prescriber gap is a cancel + resubmit.
physician.namestringrequired1 to 200 characters.
physician.npistringrequired10-digit NPI. The schema checks the digit count only, but the number is additionally validated against the CMS check digit (Luhn over "80840" plus the first 9 digits). A ten-digit number that fails the check digit is rejected with 422 prescriber_incomplete and a prescriber_npi_invalid blocker, in BOTH environments. The fulfilling pharmacy cannot fill against a malformed NPI. Pattern ^\d{10}$.
physician.deastringoptionalOptional. DEA registration number. Up to 20 characters.
physician.first_namestringoptionalPrescriber first name (preferred over splitting name). Up to 100 characters.
physician.last_namestringoptionalPrescriber last name. Up to 100 characters.
physician.credentialstringoptionale.g. MD, DO, NP, PA. Up to 16 characters.
physician.phonestringrequiredPrescriber / practice phone. Up to 32 characters.
physician.addressobjectrequiredPrescriber's practice address. Required by the pharmacy to fill.
physician.address.line1stringrequired1 to 200 characters.
physician.address.line2stringoptionalUp to 200 characters.
physician.address.citystringrequired1 to 100 characters.
physician.address.statestringrequiredTwo-letter US state code. Pattern ^[A-Za-z]{2}$.
physician.address.zipstringrequired5-digit ZIP or ZIP+4. Pattern ^\d{5}(-\d{4})?$.
itemsarray of objectrequiredAt least 1 item. At most 50 items.
items[].skustringrequired1 to 64 characters.
items[].strengthstringrequired1 to 64 characters.
items[].quantityintegerrequiredFrom 1 to 10000.
items[].directionsstringrequired1 to 1000 characters.
items[].refillsintegeroptionalFrom 0 to 99. Default 0.
billingobjectoptionalInformational. method is recorded on the payload but does not select a payment rail: invoicing terms are set per clinic on our side.
billing.methodstringrequiredOne of: ach_invoice, card, plaid, wire.
billing.account_idstringoptionalYour clinic-account identifier. It must EQUAL clinic.external_id, which is the field clinic attribution actually resolves on. Sending an account_id with no clinic.external_id, or one that disagrees with it, is rejected 422 billing_account_id_unsupported rather than silently ignored: the two would name different billable accounts and there is no precedence between them, so accepting the order would invoice and credit-cap a clinic you did not name. Send the account as clinic.external_id, or omit this field. Up to 128 characters.
clinicobjectoptionalOptional clinic identity. With an external_id, the order goes to your clinic with that id, and a clinic we have never seen is auto-created from this block on its first order (order-first onboarding), so you do not have to call POST /clinics first, EXCEPT on a partner-attested account, where the clinic must be enrolled with its attestation before its first order is accepted. Without an external_id, npi is used when exactly one of your clinics has it, and otherwise the order goes to your account's default clinic when one is configured (with none, the order has no clinic). Send the contact fields so an auto-created clinic has a real name and a reachable contact instead of a blank placeholder.
clinic.external_idstringoptionalYour own id for the clinic. 1 to 128 characters.
clinic.location_idstringoptionalThe SITE inside this clinic account the order came from, for an account that spans several locations. A short code (letters, digits, dot, dash, underscore, starting with a letter or digit), never a description. It does not create or select a clinic: the account is still resolved by external_id / npi. Stored on the order, echoed back as clinicLocation, and carried onto Axiom's reconciliation export so each site can be reconciled. It is not printed on invoices yet. Omit it for the account's default location. Pattern ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$.
clinic.location_namestringoptionalDisplay name for location_id, e.g. the site's town. Rejected (400) without location_id. 1 to 120 characters.
clinic.npistringoptionalPattern ^\d{10}$.
clinic.namestringoptional1 to 200 characters.
clinic.primary_contact_emailstring (email)optionalUp to 255 characters.
clinic.primary_contact_phonestringoptional1 to 32 characters.
clinic.pricing_tierstringoptionalPricing tier assigned to the clinic. Values are agreed with Axiom. Applied ONLY when this order auto-creates the clinic, and ignored for a clinic that already exists. One of: tier_1, tier_2, tier_3, enterprise.
external_notesstringoptionalFree-text notes for this order (special handling, clinic instructions). Stored on the order and echoed back as externalNotes on the order envelope, and visible to Axiom operators. Note these are NOT currently transmitted to the fulfilling pharmacy, so do not put fill-critical instructions here. Up to 2000 characters.
metadataobjectoptionalOpaque passthrough blob, capped at 8000 characters of serialized JSON.
Responses
| Status | Meaning |
|---|---|
201 | Order created (or idempotent replay of a prior create) |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
409 | partner_order_id_duplicate: an order with this partner_order_id already exists (the body carries its partnerOrderId and axiomOrderId). Or idempotency_key_reused: this Idempotency-Key was already used with a different request body. Or order_in_flight (with retryable: true): an identical request with this key is still being processed, so retry it shortly. If it persists, this key and partner_order_id already belong to an earlier order that is failed or cancelled: read that order first, and never resubmit one whose failure.code is pharmacy_outcome_unknown (the pharmacy may already hold the prescription). Only a resubmission that is safe goes under a NEW Idempotency-Key. |
422 | The order was REFUSED at intake. No order is created and no webhook is sent; error is a stable machine code you can branch on. Note a first-seen clinic sent in the clinic block may still have been auto-created before the order was rejected (for example by the agreement gate), so a retry can report the clinic as already known. PRESCRIBER GATE. An order missing prescriber details required to fill it is rejected with prescriber_incomplete and a blockers array naming each missing field, in BOTH sandbox and production. When anything OTHER than a prescriber field is in the list (today: patient.sex), the code is order_incomplete instead, with the same blockers shape. Sandbox applies the identical gate now, so a payload that passes this gate in sandbox passes it in production (production still adds credit limits and, on some accounts, the clinic agreement). It used to be more forgiving here, accepting in sandbox what production rejected, so do not build against that older behaviour. REMOVED PRESCRIBER (prescriber_inactive, being introduced, OFF today). When this refusal is switched on, an order whose physician.npi is on the order's clinic's list of providers, but where every entry for that NPI has been removed by the clinic or by Axiom, is rejected with prescriber_inactive and a plain message, in sandbox and production alike. An NPI that is not on the clinic's list at all is NOT refused by this rule, and another clinic's list never decides it. Resubmit under an active prescriber, or ask the clinic to add the prescriber back. Until it is switched on these orders are accepted; we will tell you before it changes. POST /orders/check reports the same code whenever create would return it. PATIENT PHONE (patient.phone): REQUIRED FROM 2026-11-20 (00:00 US Eastern). The dispensing pharmacy will not fill a prescription without a patient phone. From that date (or earlier if we tell you so), an order with no patient.phone, or one that is not a dialable number (a US number as ten digits, 406-555-0142, or E.164 such as +14065550142, no extension), is rejected with order_incomplete and a blockers entry whose field is patient.phone and whose code is patient_phone_missing or patient_phone_undialable, in sandbox and production alike. The blocker never echoes the number you sent. POST /orders/check reports the same blocker whenever create would. UNTIL THEN, such an order is accepted, and both the create 201 and POST /orders/check carry a non-blocking warnings entry with code patient_phone_required_soon, field patient.phone, and a message stating the date. It never echoes the number. An order with a dialable phone gets no such entry. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (201)
oktruerequired
axiomOrderIdintegerrequiredAxiom's internal order id.
partnerOrderIdstringrequired
orderobjectrequiredCanonical order representation echoed back on create + detail.
order.axiomOrderIdintegeroptional
order.partnerOrderIdstringoptional
order.statusstringoptionalWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
order.pharmacyIdinteger | nulloptionalAxiom's pharmacy id for the fulfiller. Null before the order routes, and null when a marketplace fulfilled it on its own side. Prefer fulfiller below, which also carries the name.
order.fulfillerobject | nulloptionalWHO is filling this order. Null until the order routes. This is the same information the order.status_changed webhook sends as pharmacy, under a different key: the webhook names the field pharmacy, this endpoint names it fulfiller, and both describe the fulfilling pharmacy. Axiom is the routing destination, not the fulfiller, so model this as a separate fulfilling pharmacy on your side, never as Axiom.
order.fulfiller.namestringrequiredPharmacy display name.
order.fulfiller.pharmacyIdinteger | nulloptionalAxiom's pharmacy id. NULL when a MARKETPLACE fulfilled the order and chose the pharmacy on its own side, in which case there is no Axiom pharmacy row to reference and only the name is meaningful.
order.fulfiller.codestringoptionalThe upstream marketplace's own code for that pharmacy. Present only for a marketplace fulfiller.
order.failureobjectoptionalPresent only when status is failed after a dispatch attempt to the pharmacy failed: a stable code and a plain-language detail. A field code (for example missing_prescriber_phone) names what to fix. pharmacy_rejected means ask Axiom support for the reason. pharmacy_outcome_unknown means the pharmacy may already hold the prescription: DO NOT resubmit it. dispatch_failed means Axiom is reviewing it.
order.failure.codestringrequired
order.failure.detailstring | nullrequired
order.patientobjectoptionalThe patient as stored on the order.
order.patient.firstNamestringoptional
order.patient.lastNamestringoptional
order.patient.dobstringoptionalYYYY-MM-DD
order.patient.sexstring | nulloptionalOne of: M, F, X, null.
order.patient.phonestring | nulloptional
order.shipToobjectoptionalThe ship-to address.
order.shipTo.line1stringoptional
order.shipTo.line2string | nulloptional
order.shipTo.citystringoptional
order.shipTo.statestringoptional
order.shipTo.zipstringoptional
order.itemsarray of objectoptional
order.items[].productIdintegeroptionalAxiom's product id for the line's SKU.
order.items[].quantityintegeroptional
order.items[].directionsstringoptional
order.items[].refillsintegeroptional
order.externalNotesstring | nulloptionalThe external_notes you sent, or null.
order.clinicLocationobject | nulloptionalThe clinic.location_id and clinic.location_name you sent, or null when none.
order.clinicLocation.idstringoptional
order.clinicLocation.namestring | nulloptional
order.shippingobjectoptionalThe shipping service level the order ships at, with its price. An order placed without shipping_method reports overnight.
order.shipping.methodstringoptionalOne of: standard, overnight.
order.shipping.labelstringoptional
order.shipping.centsintegeroptional
order.shipping.descriptionstringoptional
order.trackingNumberstring | nulloptionalCarrier tracking number. Null until the pharmacy reports it (typically on the transition into shipped). Same field name the order.status_changed webhook sends.
order.carrierstring | nulloptionalShipping carrier, e.g. UPS. Null until the pharmacy reports it.
order.shipmentsarray of objectoptionalEvery carrier tracking reference we hold for this order, INCLUDING the boxes its split legs shipped in. A multi-product order that no single pharmacy can fill is split into one child order per pharmacy, and the CHILDREN carry the tracking, so trackingNumber above describes only this order's own primary box and is null for a split order. This array is the one place a split order's boxes appear. An entry is a tracking REFERENCE, not a confirmed shipment and not a box count: an order corrected back to packing keeps its tracking on purpose, so the length of this array does not establish how much has shipped. Match entries by trackingNumber, not by position, which can change as later boxes are reported. Empty until the pharmacy reports anything.
order.shipments[].axiomOrderIdintegerrequiredThe order this reference belongs to: this order, or one of its split legs. NOT a shipment id, and not unique, since one order shipping two boxes repeats it.
order.shipments[].trackingNumberstringrequired
order.shipments[].carrierstring | nulloptionalShipping carrier, e.g. UPS.
order.reshipobjectoptionalWhether this order is one half of a RE-SHIP, and which order is the other half. A re-ship is recorded as a new Axiom order rather than as a second tracking number on the original, so each shipment keeps its own tracking, its own freight and its own billing. This object ties the two together: the re-ship reports replaces, and the order it replaced reports replacedBy. Both are null on an order that is not part of a re-ship, which is almost all of them. The order.status_changed webhook does NOT carry this object. A re-ship is a new order, so read the link from the REST order detail or the order list once you see it. An order has at most one re-ship recorded against it. A link whose other end is not yours to see reads as null.
order.reship.replacesobject | nullrequiredThe order this one replaces. Set on the re-ship.
order.reship.replaces.axiomOrderIdintegerrequiredAxiom's id for the order this one replaces.
order.reship.replaces.partnerOrderIdstringrequiredYour own order id for it.
order.reship.replacedByobject | nullrequiredThe re-ship recorded against this order. Set on the original.
order.reship.replacedBy.axiomOrderIdintegerrequiredAxiom's id for the re-ship.
order.reship.replacedBy.partnerOrderIdstringrequiredYour own order id for it.
order.routedAtstring (date-time) | nulloptional
order.shippedAtstring (date-time) | nulloptional
order.deliveredAtstring (date-time) | nulloptional
order.createdAtstring (date-time)optional
warningsarray of objectrequiredNON-BLOCKING NOTICE OF A REQUIREMENT THAT STARTS ON A STATED DATE, and becomes a 422 on that date. Today the only entry is patient_phone_required_soon (field patient.phone, the date in message, never the value), carried until 2026-11-20 by an order whose patient phone is missing or undialable. The order WAS accepted. [] for a compliant order, and always present so you can check warnings.length. Any gap that is ALREADY required is a hard 422 prescriber_incomplete or order_incomplete reject in BOTH environments, so it never lands here. Non-blocking completeness items are in advisories, not here. These were never validation errors either (those are a 400 with error: validation_failed). SANDBOX BEHAVES EXACTLY LIKE PRODUCTION. A gap that is already required is a hard reject in BOTH environments: the create returns 422 prescriber_incomplete (or order_incomplete) with the gaps in a blockers array, and no order is made. So an order that passes these checks in sandbox passes them in production (production still adds credit limits and, on some accounts, the clinic agreement), and this array never holds a refusal: only a dated notice like patient_phone_required_soon on an order that WAS accepted. This used to differ. Sandbox accepted what production rejected and reported the divergence in productionReadiness, which meant an integration could pass every sandbox test and fail 100% of its production orders on go-live day. Sandbox is no longer more forgiving than production on these checks.
warnings[].codestringrequiredStable machine code. One of prescriber_address_missing, prescriber_phone_missing, prescriber_phone_undialable, prescriber_first_name_missing, prescriber_last_name_missing, prescriber_credential_missing, prescriber_credential_unsupported, prescriber_npi_invalid, patient_sex_missing, patient_phone_missing, patient_phone_undialable, and (a non-blocking notice, only in a 201 or a check warnings array) patient_phone_required_soon.
warnings[].fieldstringrequiredDotted path of the field, e.g. physician.address.
warnings[].messagestringrequiredHuman-readable explanation.
advisoriesarray of objectoptionalCOMPLETENESS advisories, and NOT the same thing as warnings above. warnings means "production will reject this from the stated date" (today only patient_phone_required_soon; the order was accepted). These do not: they are advisory in BOTH environments and will never fail an order on their own. DO NOT SCRIPT NEEDLE TIPS. Every cartridge order ships with 25 tips as standard, so there is no need to put them on the prescription, and we no longer advise about them. A full box of 100 stays orderable as its own line for a patient who needs extra. The injector pen is still worth reporting: it is not needed on a refill unless the patient needs a new one, which is the prescriber's call, so we report what we can see and leave the decision with you. A prescription missing a pen the patient does not already have means the pharmacy phones the office. injector_pen_missing is suppressed when this patient already has a pen on their Axiom history. It is raised when we have no history to check, because a first sighting is when the pen is most likely genuinely missing. multi_shipment_expected means NO single pharmacy can fill every line on this order, so it will ship as more than one package, each carrying its own shipping fee. Raised only when a one-package fill is impossible; its absence is not a promise of a single box (state licensing can still split an order at routing time). sku names the line forcing the split. PRODUCTION orders only: sandbox orders never route, so sandbox never raises it. It arrives on the create response, AFTER the order was accepted and queued for routing. To split the order yourself instead, cancel it first and wait for status cancelled before submitting the parts, or both versions can reach pharmacies. Empty array when the order looks complete, and always present so you can check advisories.length reliably.
advisories[].codestringrequiredStable machine code. One of: injector_pen_missing, multi_shipment_expected.
advisories[].skustringrequiredThe MEDICATION line the advisory is about, so you can point at the right one.
advisories[].messagestringrequiredHuman-readable explanation.
productionReadinessobjectoptionalSANDBOX RESPONSES ONLY, and now always {wouldAccept: true, blockers: []}. RETAINED FOR COMPATIBILITY, not because it still tells you anything. It existed to warn that a sandbox 201 might have been a production 422 on these checks. Sandbox now applies the identical gate, so this field can no longer report a difference. (Production still adds credit limits and, on some accounts, the clinic agreement, which this field never covered.) An existing integration reading it keeps working; a new one should ignore it. It never was a promise the order would be routed, filled or billed: it cannot see pharmacy eligibility, inventory, credit caps, or whether your production key is enabled. That is still true.
productionReadiness.wouldAcceptbooleanrequiredTrue when production would accept this body (i.e. blockers is empty).
productionReadiness.blockersarray of objectrequiredExactly the blockers array production would return on its 422 prescriber_incomplete. Empty when wouldAccept is true.
productionReadiness.blockers[].codestringrequiredStable machine code. One of prescriber_address_missing, prescriber_phone_missing, prescriber_phone_undialable, prescriber_first_name_missing, prescriber_last_name_missing, prescriber_credential_missing, prescriber_credential_unsupported, prescriber_npi_invalid, patient_sex_missing, patient_phone_missing, patient_phone_undialable, and (a non-blocking notice, only in a 201 or a check warnings array) patient_phone_required_soon.
productionReadiness.blockers[].fieldstringrequiredDotted path of the field, e.g. physician.address.
productionReadiness.blockers[].messagestringrequiredHuman-readable explanation.
Request
http
POST /api/inbound/acme-health/orders
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>
Idempotency-Key: <your key>json
{
"partner": "acme-health",
"partner_order_id": "ACME-0001",
"idempotency_key": "8f41caf8-c8b0-4e57-9e9c-30ec25eec49c",
"shipping_method": "standard",
"patient": {
"first_name": "Jane",
"last_name": "Doe",
"dob": "1980-01-01",
"sex": "F",
"phone": "406-555-0142",
"address": {
"line1": "123 Main St",
"city": "Springfield",
"state": "TX",
"zip": "62701"
}
},
"physician": {
"name": "Dr. Alex Example",
"first_name": "Alex",
"last_name": "Example",
"credential": "MD",
"npi": "1234567893",
"phone": "406-555-0100",
"address": {
"line1": "500 Clinic Way",
"city": "Springfield",
"state": "TX",
"zip": "62701"
}
},
"clinic": {
"external_id": "acme-clinic-001",
"name": "Example Wellness Clinic"
},
"items": [
{
"sku": "EXAMPLE-SKU-1",
"strength": "10mg",
"quantity": 1,
"directions": "Use as directed.",
"refills": 0
}
]
}Response 201
json
{
"ok": true,
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"order": {
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"status": "received",
"pharmacyId": null,
"fulfiller": null,
"patient": {
"firstName": "Jane",
"lastName": "Doe",
"dob": "1980-01-01",
"sex": "F",
"phone": "406-555-0142"
},
"shipTo": {
"line1": "123 Main St",
"line2": null,
"city": "Springfield",
"state": "TX",
"zip": "62701"
},
"items": [
{
"productId": 28453,
"quantity": 1,
"directions": "Use as directed.",
"refills": 0
}
],
"externalNotes": null,
"clinicLocation": null,
"shipping": {
"method": "standard",
"label": "Standard",
"cents": 1000,
"description": "Ships in a standard box, arrives in 3 to 5 business days."
},
"trackingNumber": null,
"carrier": null,
"shipments": [],
"reship": {
"replaces": null,
"replacedBy": null
},
"routedAt": null,
"shippedAt": null,
"deliveredAt": null,
"createdAt": "2026-10-25T18:57:41.840Z"
},
"warnings": [],
"advisories": [],
"productionReadiness": {
"wouldAccept": true,
"blockers": []
}
}Get an order with its event timeline
GET/api/inbound/{partnerSlug}/orders/{partnerOrderId}
Keyed by your own partner_order_id. The numeric axiomOrderId from the create response is accepted too, scoped to your partner and key environment, so polling with either id works. Your own reference is matched first, so if you use bare integers as partner_order_id, poll with those.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
partnerOrderIdstringpathrequiredThe partner's own order id, as sent in partner_order_id on create.
Responses
| Status | Meaning |
|---|---|
200 | Order detail |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
Response fields (200)
oktruerequired
orderobjectrequiredCanonical order representation echoed back on create + detail.
order.axiomOrderIdintegeroptional
order.partnerOrderIdstringoptional
order.statusstringoptionalWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
order.pharmacyIdinteger | nulloptionalAxiom's pharmacy id for the fulfiller. Null before the order routes, and null when a marketplace fulfilled it on its own side. Prefer fulfiller below, which also carries the name.
order.fulfillerobject | nulloptionalWHO is filling this order. Null until the order routes. This is the same information the order.status_changed webhook sends as pharmacy, under a different key: the webhook names the field pharmacy, this endpoint names it fulfiller, and both describe the fulfilling pharmacy. Axiom is the routing destination, not the fulfiller, so model this as a separate fulfilling pharmacy on your side, never as Axiom.
order.fulfiller.namestringrequiredPharmacy display name.
order.fulfiller.pharmacyIdinteger | nulloptionalAxiom's pharmacy id. NULL when a MARKETPLACE fulfilled the order and chose the pharmacy on its own side, in which case there is no Axiom pharmacy row to reference and only the name is meaningful.
order.fulfiller.codestringoptionalThe upstream marketplace's own code for that pharmacy. Present only for a marketplace fulfiller.
order.failureobjectoptionalPresent only when status is failed after a dispatch attempt to the pharmacy failed: a stable code and a plain-language detail. A field code (for example missing_prescriber_phone) names what to fix. pharmacy_rejected means ask Axiom support for the reason. pharmacy_outcome_unknown means the pharmacy may already hold the prescription: DO NOT resubmit it. dispatch_failed means Axiom is reviewing it.
order.failure.codestringrequired
order.failure.detailstring | nullrequired
order.patientobjectoptionalThe patient as stored on the order.
order.patient.firstNamestringoptional
order.patient.lastNamestringoptional
order.patient.dobstringoptionalYYYY-MM-DD
order.patient.sexstring | nulloptionalOne of: M, F, X, null.
order.patient.phonestring | nulloptional
order.shipToobjectoptionalThe ship-to address.
order.shipTo.line1stringoptional
order.shipTo.line2string | nulloptional
order.shipTo.citystringoptional
order.shipTo.statestringoptional
order.shipTo.zipstringoptional
order.itemsarray of objectoptional
order.items[].productIdintegeroptionalAxiom's product id for the line's SKU.
order.items[].quantityintegeroptional
order.items[].directionsstringoptional
order.items[].refillsintegeroptional
order.externalNotesstring | nulloptionalThe external_notes you sent, or null.
order.clinicLocationobject | nulloptionalThe clinic.location_id and clinic.location_name you sent, or null when none.
order.clinicLocation.idstringoptional
order.clinicLocation.namestring | nulloptional
order.shippingobjectoptionalThe shipping service level the order ships at, with its price. An order placed without shipping_method reports overnight.
order.shipping.methodstringoptionalOne of: standard, overnight.
order.shipping.labelstringoptional
order.shipping.centsintegeroptional
order.shipping.descriptionstringoptional
order.trackingNumberstring | nulloptionalCarrier tracking number. Null until the pharmacy reports it (typically on the transition into shipped). Same field name the order.status_changed webhook sends.
order.carrierstring | nulloptionalShipping carrier, e.g. UPS. Null until the pharmacy reports it.
order.shipmentsarray of objectoptionalEvery carrier tracking reference we hold for this order, INCLUDING the boxes its split legs shipped in. A multi-product order that no single pharmacy can fill is split into one child order per pharmacy, and the CHILDREN carry the tracking, so trackingNumber above describes only this order's own primary box and is null for a split order. This array is the one place a split order's boxes appear. An entry is a tracking REFERENCE, not a confirmed shipment and not a box count: an order corrected back to packing keeps its tracking on purpose, so the length of this array does not establish how much has shipped. Match entries by trackingNumber, not by position, which can change as later boxes are reported. Empty until the pharmacy reports anything.
order.shipments[].axiomOrderIdintegerrequiredThe order this reference belongs to: this order, or one of its split legs. NOT a shipment id, and not unique, since one order shipping two boxes repeats it.
order.shipments[].trackingNumberstringrequired
order.shipments[].carrierstring | nulloptionalShipping carrier, e.g. UPS.
order.reshipobjectoptionalWhether this order is one half of a RE-SHIP, and which order is the other half. A re-ship is recorded as a new Axiom order rather than as a second tracking number on the original, so each shipment keeps its own tracking, its own freight and its own billing. This object ties the two together: the re-ship reports replaces, and the order it replaced reports replacedBy. Both are null on an order that is not part of a re-ship, which is almost all of them. The order.status_changed webhook does NOT carry this object. A re-ship is a new order, so read the link from the REST order detail or the order list once you see it. An order has at most one re-ship recorded against it. A link whose other end is not yours to see reads as null.
order.reship.replacesobject | nullrequiredThe order this one replaces. Set on the re-ship.
order.reship.replaces.axiomOrderIdintegerrequiredAxiom's id for the order this one replaces.
order.reship.replaces.partnerOrderIdstringrequiredYour own order id for it.
order.reship.replacedByobject | nullrequiredThe re-ship recorded against this order. Set on the original.
order.reship.replacedBy.axiomOrderIdintegerrequiredAxiom's id for the re-ship.
order.reship.replacedBy.partnerOrderIdstringrequiredYour own order id for it.
order.routedAtstring (date-time) | nulloptional
order.shippedAtstring (date-time) | nulloptional
order.deliveredAtstring (date-time) | nulloptional
order.createdAtstring (date-time)optional
eventsarray of objectrequired
events[].statusstringrequiredWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
events[].sourcestringrequiredWhere the change came from: pharmacy for anything the pharmacy reported, sandbox for the simulated sandbox lifecycle, or an Axiom-side source such as marketplace_inbound, routing_worker or admin_manual.
events[].createdAtstring (date-time)required
events[].payloadobject | nulloptional
Request
http
GET /api/inbound/acme-health/orders/{partnerOrderId}
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 200
json
{
"ok": true,
"order": {
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"status": "received",
"pharmacyId": null,
"fulfiller": null,
"patient": {
"firstName": "Jane",
"lastName": "Doe",
"dob": "1980-01-01",
"sex": "F",
"phone": "406-555-0142"
},
"shipTo": {
"line1": "123 Main St",
"line2": null,
"city": "Springfield",
"state": "TX",
"zip": "62701"
},
"items": [
{
"productId": 28453,
"quantity": 1,
"directions": "Use as directed.",
"refills": 0
}
],
"externalNotes": null,
"clinicLocation": null,
"shipping": {
"method": "standard",
"label": "Standard",
"cents": 1000,
"description": "Ships in a standard box, arrives in 3 to 5 business days."
},
"trackingNumber": null,
"carrier": null,
"shipments": [],
"reship": {
"replaces": null,
"replacedBy": null
},
"routedAt": null,
"shippedAt": null,
"deliveredAt": null,
"createdAt": "2026-10-25T18:57:41.840Z"
},
"events": [
{
"status": "received",
"source": "marketplace_inbound",
"createdAt": "2026-10-25T18:57:41.840Z",
"payload": null
}
]
}List orders
GET/api/inbound/{partnerSlug}/orders
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
statusstringqueryoptionalOne of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
sincestring (date-time)queryoptional
limitintegerqueryoptionalFrom 1 to 500. Default 100.
offsetintegerqueryoptionalAt least 0. Default 0.
Responses
| Status | Meaning |
|---|---|
200 | Orders for the partner |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
ordersarray of objectrequired
orders[].axiomOrderIdintegerrequired
orders[].partnerOrderIdstringrequired
orders[].statusstringrequiredWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
orders[].trackingNumberstring | nulloptionalCarrier tracking number. Null until the pharmacy reports it.
orders[].carrierstring | nulloptionalShipping carrier, e.g. UPS.
orders[].shipmentsarray of objectoptionalEvery carrier tracking reference we hold for this order, INCLUDING the boxes its split legs shipped in. A multi-product order that no single pharmacy can fill is split into one child order per pharmacy, and the CHILDREN carry the tracking, so trackingNumber above describes only this order's own primary box and is null for a split order. This array is the one place a split order's boxes appear. An entry is a tracking REFERENCE, not a confirmed shipment and not a box count: an order corrected back to packing keeps its tracking on purpose, so the length of this array does not establish how much has shipped. Match entries by trackingNumber, not by position, which can change as later boxes are reported. Empty until the pharmacy reports anything.
orders[].shipments[].axiomOrderIdintegerrequiredThe order this reference belongs to: this order, or one of its split legs. NOT a shipment id, and not unique, since one order shipping two boxes repeats it.
orders[].shipments[].trackingNumberstringrequired
orders[].shipments[].carrierstring | nulloptionalShipping carrier, e.g. UPS.
orders[].reshipobjectoptionalWhether this order is one half of a RE-SHIP, and which order is the other half. A re-ship is recorded as a new Axiom order rather than as a second tracking number on the original, so each shipment keeps its own tracking, its own freight and its own billing. This object ties the two together: the re-ship reports replaces, and the order it replaced reports replacedBy. Both are null on an order that is not part of a re-ship, which is almost all of them. The order.status_changed webhook does NOT carry this object. A re-ship is a new order, so read the link from the REST order detail or the order list once you see it. An order has at most one re-ship recorded against it. A link whose other end is not yours to see reads as null.
orders[].reship.replacesobject | nullrequiredThe order this one replaces. Set on the re-ship.
orders[].reship.replaces.axiomOrderIdintegerrequiredAxiom's id for the order this one replaces.
orders[].reship.replaces.partnerOrderIdstringrequiredYour own order id for it.
orders[].reship.replacedByobject | nullrequiredThe re-ship recorded against this order. Set on the original.
orders[].reship.replacedBy.axiomOrderIdintegerrequiredAxiom's id for the re-ship.
orders[].reship.replacedBy.partnerOrderIdstringrequiredYour own order id for it.
orders[].shippedAtstring (date-time) | nulloptional
orders[].createdAtstring (date-time)required
Request
http
GET /api/inbound/acme-health/orders
Host: api.axiomscript.com
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>Response 200
json
{
"ok": true,
"orders": [
{
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"status": "received",
"trackingNumber": null,
"carrier": null,
"shipments": [],
"reship": {
"replaces": null,
"replacedBy": null
},
"shippedAt": null,
"createdAt": "2026-10-25T18:57:41.840Z"
}
]
}Edit a not-yet-routed order
PATCH/api/inbound/{partnerSlug}/orders/{partnerOrderId}
Correct the patient name/phone or the shipping address on an order that has not been sent to a pharmacy yet. Only valid while the order is still received; once it routes, the pharmacy holds the prescription and the edit would silently no-op, so it is refused with 409 order_not_editable. Split orders (a parent or any child) are also refused. Send at least one field. Anything not listed here (items, pricing) is rejected with a 400 rather than silently ignored. Idempotent via the Idempotency-Key header.
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
partnerOrderIdstringpathrequiredThe partner's own order id, as sent in partner_order_id on create.
Idempotency-KeystringheaderrequiredREQUIRED on every endpoint that creates or changes something (see the Idempotency section of the API description for the list). Omitting it is a 400 idempotency_key_required. Replaying the same key with the SAME body returns the original response. Reusing a key with a DIFFERENT body is a 409 idempotency_key_reused. Max 128 characters.
Request body
patientobjectoptional
patient.first_namestringoptional1 to 100 characters.
patient.last_namestringoptional1 to 100 characters.
patient.phonestringoptionalUp to 32 characters.
shippingobjectoptional
shipping.line1stringoptional1 to 200 characters.
shipping.line2stringoptionalUp to 200 characters.
shipping.citystringoptional1 to 100 characters.
shipping.statestringoptional2 to 2 characters. Pattern ^[A-Za-z]{2}$.
shipping.zipstringoptionalPattern ^\d{5}(-\d{4})?$.
Responses
| Status | Meaning |
|---|---|
200 | Order updated |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
409 | Either order_not_editable (the order left received, or it is a split parent/child) or idempotency_key_reused (this key was already used with a different body). |
422 | The edited values are not orderable. Editing shipping.state re-runs the same ship-state checks as create, so it can return ship_state_not_licensed or product_unavailable_in_state. |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
axiomOrderIdintegerrequired
partnerOrderIdstringrequired
statusstringrequiredWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
orderobjectrequiredCanonical order representation echoed back on create + detail.
order.axiomOrderIdintegeroptional
order.partnerOrderIdstringoptional
order.statusstringoptionalWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
order.pharmacyIdinteger | nulloptionalAxiom's pharmacy id for the fulfiller. Null before the order routes, and null when a marketplace fulfilled it on its own side. Prefer fulfiller below, which also carries the name.
order.fulfillerobject | nulloptionalWHO is filling this order. Null until the order routes. This is the same information the order.status_changed webhook sends as pharmacy, under a different key: the webhook names the field pharmacy, this endpoint names it fulfiller, and both describe the fulfilling pharmacy. Axiom is the routing destination, not the fulfiller, so model this as a separate fulfilling pharmacy on your side, never as Axiom.
order.fulfiller.namestringrequiredPharmacy display name.
order.fulfiller.pharmacyIdinteger | nulloptionalAxiom's pharmacy id. NULL when a MARKETPLACE fulfilled the order and chose the pharmacy on its own side, in which case there is no Axiom pharmacy row to reference and only the name is meaningful.
order.fulfiller.codestringoptionalThe upstream marketplace's own code for that pharmacy. Present only for a marketplace fulfiller.
order.failureobjectoptionalPresent only when status is failed after a dispatch attempt to the pharmacy failed: a stable code and a plain-language detail. A field code (for example missing_prescriber_phone) names what to fix. pharmacy_rejected means ask Axiom support for the reason. pharmacy_outcome_unknown means the pharmacy may already hold the prescription: DO NOT resubmit it. dispatch_failed means Axiom is reviewing it.
order.failure.codestringrequired
order.failure.detailstring | nullrequired
order.patientobjectoptionalThe patient as stored on the order.
order.patient.firstNamestringoptional
order.patient.lastNamestringoptional
order.patient.dobstringoptionalYYYY-MM-DD
order.patient.sexstring | nulloptionalOne of: M, F, X, null.
order.patient.phonestring | nulloptional
order.shipToobjectoptionalThe ship-to address.
order.shipTo.line1stringoptional
order.shipTo.line2string | nulloptional
order.shipTo.citystringoptional
order.shipTo.statestringoptional
order.shipTo.zipstringoptional
order.itemsarray of objectoptional
order.items[].productIdintegeroptionalAxiom's product id for the line's SKU.
order.items[].quantityintegeroptional
order.items[].directionsstringoptional
order.items[].refillsintegeroptional
order.externalNotesstring | nulloptionalThe external_notes you sent, or null.
order.clinicLocationobject | nulloptionalThe clinic.location_id and clinic.location_name you sent, or null when none.
order.clinicLocation.idstringoptional
order.clinicLocation.namestring | nulloptional
order.shippingobjectoptionalThe shipping service level the order ships at, with its price. An order placed without shipping_method reports overnight.
order.shipping.methodstringoptionalOne of: standard, overnight.
order.shipping.labelstringoptional
order.shipping.centsintegeroptional
order.shipping.descriptionstringoptional
order.trackingNumberstring | nulloptionalCarrier tracking number. Null until the pharmacy reports it (typically on the transition into shipped). Same field name the order.status_changed webhook sends.
order.carrierstring | nulloptionalShipping carrier, e.g. UPS. Null until the pharmacy reports it.
order.shipmentsarray of objectoptionalEvery carrier tracking reference we hold for this order, INCLUDING the boxes its split legs shipped in. A multi-product order that no single pharmacy can fill is split into one child order per pharmacy, and the CHILDREN carry the tracking, so trackingNumber above describes only this order's own primary box and is null for a split order. This array is the one place a split order's boxes appear. An entry is a tracking REFERENCE, not a confirmed shipment and not a box count: an order corrected back to packing keeps its tracking on purpose, so the length of this array does not establish how much has shipped. Match entries by trackingNumber, not by position, which can change as later boxes are reported. Empty until the pharmacy reports anything.
order.shipments[].axiomOrderIdintegerrequiredThe order this reference belongs to: this order, or one of its split legs. NOT a shipment id, and not unique, since one order shipping two boxes repeats it.
order.shipments[].trackingNumberstringrequired
order.shipments[].carrierstring | nulloptionalShipping carrier, e.g. UPS.
order.reshipobjectoptionalWhether this order is one half of a RE-SHIP, and which order is the other half. A re-ship is recorded as a new Axiom order rather than as a second tracking number on the original, so each shipment keeps its own tracking, its own freight and its own billing. This object ties the two together: the re-ship reports replaces, and the order it replaced reports replacedBy. Both are null on an order that is not part of a re-ship, which is almost all of them. The order.status_changed webhook does NOT carry this object. A re-ship is a new order, so read the link from the REST order detail or the order list once you see it. An order has at most one re-ship recorded against it. A link whose other end is not yours to see reads as null.
order.reship.replacesobject | nullrequiredThe order this one replaces. Set on the re-ship.
order.reship.replaces.axiomOrderIdintegerrequiredAxiom's id for the order this one replaces.
order.reship.replaces.partnerOrderIdstringrequiredYour own order id for it.
order.reship.replacedByobject | nullrequiredThe re-ship recorded against this order. Set on the original.
order.reship.replacedBy.axiomOrderIdintegerrequiredAxiom's id for the re-ship.
order.reship.replacedBy.partnerOrderIdstringrequiredYour own order id for it.
order.routedAtstring (date-time) | nulloptional
order.shippedAtstring (date-time) | nulloptional
order.deliveredAtstring (date-time) | nulloptional
order.createdAtstring (date-time)optional
Request
http
PATCH /api/inbound/acme-health/orders/{partnerOrderId}
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>
Idempotency-Key: <your key>json
{
"patient": {
"phone": "406-555-0199"
}
}Response 200
json
{
"ok": true,
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"status": "received",
"order": {
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"status": "received",
"pharmacyId": null,
"fulfiller": null,
"patient": {
"firstName": "Jane",
"lastName": "Doe",
"dob": "1980-01-01",
"sex": "F",
"phone": "406-555-0199"
},
"shipTo": {
"line1": "123 Main St",
"line2": null,
"city": "Springfield",
"state": "TX",
"zip": "62701"
},
"items": [
{
"productId": 28453,
"quantity": 1,
"directions": "Use as directed.",
"refills": 0
}
],
"externalNotes": null,
"clinicLocation": null,
"shipping": {
"method": "standard",
"label": "Standard",
"cents": 1000,
"description": "Ships in a standard box, arrives in 3 to 5 business days."
},
"trackingNumber": null,
"carrier": null,
"shipments": [],
"reship": {
"replaces": null,
"replacedBy": null
},
"routedAt": null,
"shippedAt": null,
"deliveredAt": null,
"createdAt": "2026-10-25T18:57:41.840Z"
}
}Cancel an order
POST/api/inbound/{partnerSlug}/orders/{partnerOrderId}/cancel
Parameters
partnerSlugstringpathrequiredThe partner's Axiom-issued slug (e.g. acme-health).
partnerOrderIdstringpathrequiredThe partner's own order id, as sent in partner_order_id on create.
Idempotency-KeystringheaderrequiredREQUIRED on every endpoint that creates or changes something (see the Idempotency section of the API description for the list). Omitting it is a 400 idempotency_key_required. Replaying the same key with the SAME body returns the original response. Reusing a key with a DIFFERENT body is a 409 idempotency_key_reused. Max 128 characters.
Request body
reasonstringoptionalUp to 500 characters.
Responses
| Status | Meaning |
|---|---|
200 | Order cancelled (or idempotent replay) |
400 | The request was malformed. validation_failed means the body or query failed schema validation (see issues). On a mutating endpoint you can also get idempotency_key_required (no Idempotency-Key header), idempotency_key_invalid (empty, or longer than 128 characters), or idempotency_key_mismatch (the header and the body's idempotency_key are different strings). |
401 | Missing, expired, or invalid HMAC signature / timestamp |
403 | production_not_enabled. You signed with a production key, but Axiom has not yet flipped the production gate on your partner account. Holding a production key is deliberately not enough to place live orders. Contact Axiom to enable it. |
404 | No such resource for your account in this environment. On order endpoints error is not_found on GET and order_not_found on PATCH and cancel. On the ticket endpoints not_found also means support tickets are not enabled for your account. |
409 | Either order_not_cancellable (the order is already too far along) or idempotency_key_reused (this key was already used with a different body). |
423 | partner_inactive or tenant_suspended. Your partner account is deactivated or suspended, so no request will be served until it is reinstated. |
429 | Per-partner rate limit exceeded |
Response fields (200)
oktruerequired
axiomOrderIdintegerrequired
partnerOrderIdstringrequired
statusstringrequiredWhere an order is. The values are listed in the order an order usually moves through them, but the movement is not strictly forward and not every step is reported: an order can jump from routed to a later state, can go back to received when a pharmacy declines it and we reroute, and a failed order can return to routed when Axiom retries or reconciles it. Never require each step to appear, and treat any value you do not recognise as still in progress. In sandbox, routing, shipping and delivery are simulated: no pharmacy is assigned and the tracking number is a generated SANDBOX- value. received: accepted, and waiting to be routed or rerouted. A SPLIT order (one no single pharmacy can fill, fulfilled as one child order per pharmacy) stays received while its children are in progress, so a child can read shipped while the parent still reads received. The parent moves to shipped once every child is settled: shipped or delivered, or failed or cancelled with its lines credited. So a split parent reading shipped does NOT mean every line shipped; shipments and the billing feed say what did. routed: a pharmacy is assigned. Sending it to them may still be pending, held, or done, so this alone does not mean the pharmacy has accepted it. A dispatch problem we are holding for a person to resolve leaves the order routed, with no failure webhook. in_production: the pharmacy is working on it, which includes their data entry, clarification and queueing as well as compounding. ready_to_pack, ready_to_ship: packing and shipping preparation. Often inferred from a later report rather than reported on their own. shipped: shipment reported by the pharmacy or the carrier, or recorded by Axiom staff. trackingNumber and carrier are normally set from here. An order shipping in more than one box reads shipped, and shipments lists the tracking numbers currently attached to it and its split children. A RE-SHIP is a new order rather than a second tracking number on the original, so the two are tied together by reship on the order itself: the re-ship reports replaces, the original reports replacedBy. Read it from GET .../orders/{partnerOrderId} or from the order list. The order.status_changed webhook does NOT carry it today, so poll the new order once when you see it, or read the link off the list you already poll. delivered: delivery reported by the carrier or the pharmacy, or recorded by Axiom staff. Simulated in sandbox. cancelled: cancelled by you, by Axiom, or by the pharmacy. This can happen after production has started. Treat it as final; in rare cases a pharmacy reverses its own void and the order resumes, with a further order.status_changed. failed: Axiom recorded a routing, dispatch or pharmacy-reported failure. A dispatch failure can be ambiguous (the pharmacy may still have received the prescription), which is why Axiom sometimes reconciles a failed order back to routed. Routing and dispatch failures that set failed send the order.failed webhook; a failure the PHARMACY reports sends no webhook today, so it shows only when you read the order or the order list. failed is never the newStatus of an order.status_changed event, but can be the oldStatus of a later one. You can cancel an order with POST .../orders/{partnerOrderId}/cancel only while it is received or routed and it has not been sent to the pharmacy (no pharmacy order number recorded and no send on record, including one the pharmacy accepted without returning a number) or, for a split order, none of its children has. A split order's children cannot be cancelled on their own. Otherwise the answer is 409 order_not_cancellable; when the order or a child was already sent, the body carries reason (dispatched or child_dispatched) and a plain-words message. One of: received, routed, in_production, ready_to_pack, ready_to_ship, shipped, delivered, cancelled, failed.
Request
http
POST /api/inbound/acme-health/orders/{partnerOrderId}/cancel
Host: api.axiomscript.com
Content-Type: application/json
X-Axiom-Timestamp: 1767225600
X-Axiom-Signature: sha256=<hex>
Idempotency-Key: <your key>json
{
"reason": "Patient changed their mind"
}Response 200
json
{
"ok": true,
"axiomOrderId": 184028,
"partnerOrderId": "ACME-0001",
"status": "cancelled"
}
