Skip to content

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 (shipped or delivered). 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/check shows), 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 (standard or overnight). Orders that ship together in one box share one shipping charge, at the stricter of their levels. The check's estimate shows 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) or 422 partner_overdue (you pay). The body carries overdueSince and graceDays, 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 carries capCents, outstandingCents, orderChargeCents and projectedCents. 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 /invoices lists your invoices, newest first, filterable by status (issued, pending, partial, paid, dispute, late, void) and paged with limit (up to 100) and offset.
  • 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.

RequestReturns
GET /billing/ordersOrders 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.

ParameterMeaning
from, toOrder 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.
changedSinceISO 8601 time with an offset. Orders whose billing changed at or after it, sorted by billingChangedAt. Page with afterId.
invoiceNumberOrders billed on that invoice.
limit, offsetPage 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 ​

FieldMeaning
axiomOrderId, partnerOrderId, orderedAt, statusThe order.
replacesPartnerOrderIdFor a re-ship, your id of the order it replaces.
itemsLines with productId, sku, quantity and unitPriceCents. Match on productId; sku is the product's current SKU.
chargeCentsThe goods charge, before coupon, excluding shipping.
couponCentsThe coupon discount (0 when none).
shippingCentsShipping 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.
platformFeeCentsYour platform fee on the order. null means not recorded; 0 means computed as zero.
billing.statebilled, voided, held, cancelled or not_billed_yet (below).
billing.invoiceNumber, billing.invoicedAt, billing.voidedAtWhere and when it was invoiced or voided.
billing.invoiceWideCreditsCredits 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.
creditsCredits recorded on the order before it was invoiced; applied: true once an invoice used them.
invoiceCreditsCredit memos against the invoice for this order. A reversal is negative.
billingChangedAtWhen anything about this order's billing last changed, to the millisecond.
billing.stateMeans
billedOn an issued invoice. An invoice wins over a later cancel; any credit for it is in invoiceCredits.
voidedIts invoice was voided and it will not be billed again.
heldWe excluded it from billing (for example a comp or a replacement). It is not billed while held.
cancelledCancelled or failed before it was invoiced.
not_billed_yetNone of the above. It bills on the first weekly run after it ships.

Polling for changes ​

Loading diagram...
  1. Request changedSince=<watermark> without afterId.
  2. While a page comes back full, ask for the next one with changedSince set to the last row's billingChangedAt and afterId to its axiomOrderId.
  3. Store the largest billingChangedAt you 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/null

The 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.