Appearance
Billing feed and reconciliation
Summary: orders are invoiced once they ship, on a weekly cycle. Depending on how your account is set up, the invoice goes either to each clinic or to you. Ordering pauses for a payer with an invoice past its due date, and, when clinics pay, an order that would take a clinic over its credit limit is refused. To reconcile, poll GET /billing/orders?changedSince=... for what each order charged and whether and where it was billed.
How billing works
- Who pays. Each partner account is set up with a payer: either the clinic that placed the order or you, the partner. We agree this with you when your account is created. When you are the payer, your invoices are readable through the API (below).
- When an order is billed. An order becomes billable once it has shipped (
shippedordelivered). Invoices are issued automatically once a week, on Mondays (US Eastern), covering every billable order not yet invoiced. Sandbox orders are never billed. - What an order charges. The goods at your price for that clinic (the unit price
POST /orders/checkshows), less any prepaid units your account holds for that product (a balance shared by all of your clinics), less any coupon, plus shipping at the level you chose (standardorovernight). Orders that ship together in one box share one shipping charge, at the stricter of their levels. The check'sestimateshows each part (goodsCents,prepaidAppliedCents,couponCents,shippingCents,totalCents). - Overdue invoices pause ordering. When the payer has an invoice that is more than one day past its due date, new orders are refused with
422 clinic_overdue(clinic pays) or422 partner_overdue(you pay). The body carriesoverdueSinceandgraceDays, and usually the invoice number, its remaining balance and due date. Ordering resumes as soon as it is paid. - Credit limits. When clinics are the payer, each clinic has a credit limit, counting its unpaid invoices and its not-yet-invoiced orders. An order that would go over it is refused with
422 clinic_over_credit_cap, which carriescapCents,outstandingCents,orderChargeCentsandprojectedCents. It is not accepted and held. Separately, your account can have an overall limit (422 partner_exposure_cap). Clinic limits do not apply when you are the payer, and sandbox applies no credit limits at all.
POST /orders/check reports all of these before you take payment.
Which clinic an order is billed to
An order is attributed to a clinic by clinic.external_id. Without one, we use clinic.npi when exactly one of your clinics has it, and otherwise your account's default clinic if one is configured. Always send clinic.external_id. See Clinics and agreements.
billing.account_id in an order is not a second way to choose the clinic. Leave it out, or send exactly the same value as clinic.external_id. Any other combination is refused with 422 billing_account_id_unsupported rather than risk billing a clinic you did not name; details.clinicExternalIdPresent says which rule was broken. billing.method is informational only: payment terms are set per clinic on our side.
Invoices
When you are the payer:
GET /invoiceslists your invoices, newest first, filterable bystatus(issued,pending,partial,paid,dispute,late,void) and paged withlimit(up to 100) andoffset.GET /invoices/{invoiceNumber}returns one.
Each invoice has invoiceNumber, status, issuedAt, dueAt, paidAt, totalCents, paidCents, creditCents, residualCents (what is still owed), currency and paymentUrl, a hosted payment link. paymentUrl can be null even on an invoice that is still owed (for example while it is being reviewed or its balance is being updated), so read residualCents for what is owed, not the link. Line items are not included; use the billing feed for per-order detail. With a sandbox key the list is always empty.
Subscribe to invoice.issued, invoice.paid and invoice.voided to hear about changes. See Webhooks.
The billing feed
The billing feed answers, per order: what did it charge, was it discounted, credited, held or never billed, and on which invoice. It covers your own production orders, including cancelled and failed ones, whoever the payer is. It never contains patient, prescriber or ship-to details, or any free-text reason.
| Request | Returns |
|---|---|
GET /billing/orders | Orders by date window, by change, or by invoice (below). |
GET /billing/orders/{partnerOrderId} | One order, as { "ok": true, "order": {...} }, or 404 not_found. |
With a sandbox key the list is empty and the single lookup is 404.
Filters
Give at most one of these. With none, you get the last 30 days by order date.
| Parameter | Meaning |
|---|---|
from, to | Order dates as YYYY-MM-DD, US Eastern calendar days, inclusive. Either alone works. The window may span at most 92 days. Sorted by order date. |
changedSince | ISO 8601 time with an offset. Orders whose billing changed at or after it, sorted by billingChangedAt. Page with afterId. |
invoiceNumber | Orders billed on that invoice. |
limit, offset | Page size 1 to 200 (default 100) and offset. offset is not allowed with changedSince. |
Any unknown parameter, more than one filter, or a window over 92 days is 400 invalid_query with issues.
Each order
| Field | Meaning |
|---|---|
axiomOrderId, partnerOrderId, orderedAt, status | The order. |
replacesPartnerOrderId | For a re-ship, your id of the order it replaces. |
items | Lines with productId, sku, quantity and unitPriceCents. Match on productId; sku is the product's current SKU. |
chargeCents | The goods charge, before coupon, excluding shipping. |
couponCents | The coupon discount (0 when none). |
shippingCents | Shipping billed for this order. Set only when the order is billed. When several of your orders shipped in one box, one carries the fee and the others show 0. |
platformFeeCents | Your platform fee on the order. null means not recorded; 0 means computed as zero. |
billing.state | billed, voided, held, cancelled or not_billed_yet (below). |
billing.invoiceNumber, billing.invoicedAt, billing.voidedAt | Where and when it was invoiced or voided. |
billing.invoiceWideCredits | Credits applied to the whole invoice, repeated on every order of that invoice: book them once per invoice, not per order. Shown only when every order on that invoice is yours; on an invoice shared with orders from elsewhere the list is empty even if such a credit exists. |
credits | Credits recorded on the order before it was invoiced; applied: true once an invoice used them. |
invoiceCredits | Credit memos against the invoice for this order. A reversal is negative. |
billingChangedAt | When anything about this order's billing last changed, to the millisecond. |
billing.state | Means |
|---|---|
billed | On an issued invoice. An invoice wins over a later cancel; any credit for it is in invoiceCredits. |
voided | Its invoice was voided and it will not be billed again. |
held | We excluded it from billing (for example a comp or a replacement). It is not billed while held. |
cancelled | Cancelled or failed before it was invoiced. |
not_billed_yet | None of the above. It bills on the first weekly run after it ships. |
Polling for changes
Loading diagram...
- Request
changedSince=<watermark>withoutafterId. - While a page comes back full, ask for the next one with
changedSinceset to the last row'sbillingChangedAtandafterIdto itsaxiomOrderId. - Store the largest
billingChangedAtyou saw.
Start each poll from your stored watermark minus about 15 minutes and treat every order as an upsert by axiomOrderId: a change becomes visible when it commits, which can be a moment after its timestamp, and the overlap catches it. billingChangedAt can also move for a change that moved no money. As a backstop, pull the last 92 days by date window once a night.
mjs
// billing-poll.mjs: pull every order whose billing changed since your stored watermark.
import { axiom } from "./axiom-client.mjs";
let since = process.env.WATERMARK ?? "2026-10-01T00:00:00Z"; // your stored watermark, minus about 15 minutes
let afterId;
let newest = since;
for (;;) {
const q = new URLSearchParams({ changedSince: since, limit: "200" });
if (afterId) q.set("afterId", String(afterId));
const { status, body } = await axiom("GET", `/billing/orders?${q}`);
if (status !== 200) throw new Error(`billing feed answered ${status}: ${JSON.stringify(body)}`);
for (const o of body.orders) {
console.log(o.partnerOrderId, o.billing.state, o.chargeCents); // upsert by o.axiomOrderId here
if (o.billingChangedAt > newest) newest = o.billingChangedAt;
}
if (body.orders.length < 200) break;
const last = body.orders.at(-1);
since = last.billingChangedAt;
afterId = last.axiomOrderId;
}
console.log("store as the next watermark:", newest);bash
./axiom-request.sh GET "/billing/orders?changedSince=2026-10-01T00%3A00%3A00Z&limit=200" < /dev/nullThe query string is part of the signature, so sign the exact string you send. Percent-encode the timestamp (: as %3A, + as %2B): a raw + in a query string means a space.

