List invoices
const url = 'http://localhost:8080/v1/invoices?dir=asc&limit=50&offset=0';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'http://localhost:8080/v1/invoices?dir=asc&limit=50&offset=0' \ --header 'Authorization: Bearer <token>'Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”Case-insensitive substring match on invoice number.
Lower bound on created_at, inclusive. RFC3339 instant or bare YYYY-MM-DD (anchored at 00:00:00Z).
Upper bound on created_at, inclusive. RFC3339 instant or bare YYYY-MM-DD (anchored at 23:59:59.999999Z, the end of that day).
When true, restrict to open invoices past their due date that haven’t settled (finalized + due_at < now + payment_status not succeeded/processing). Backs the dashboard’s “Past due” segment.
Comma-separated invoice IDs to fetch exactly.
One of invoice_number, amount_due_cents, billing_period_start, due_at, issued_at, status, payment_status, created_at (default). amount_due_cents sorts a paid invoice by what paid it (amount_paid_cents plus credits_applied_cents, since paying zeroes amount_due_cents) and every other invoice by amount_due_cents, which a voided or written-off invoice keeps.
Rows per page, from 1 to 100. Default 50. Any other value, or one
that is not a whole number, is a 422 with param: limit.
Rows to skip, 0 or more. Default 0. A negative value, or one that is
not a whole number, is a 422 with param: offset.
Responses
Section titled “Responses”Invoice list
object
A Hardcount invoice. Returned from create, finalize, void, list, get, rotate-public-token, and collect-payment endpoints.
object
Owning tenant. Returned for diagnostic clarity but redundant with the API key (which already determines the tenant). Omitted when empty.
Empty for one-off invoices not tied to a subscription.
Set when the invoice is created, including drafts.
Lifecycle state of the invoice. uncollectible marks an invoice
a dunning policy has given up collecting (no further retries) while
keeping it on the books — distinct from voided (cancelled, owes
nothing).
Payment-attempt status. unknown marks a Stripe charge attempt that
returned an ambiguous error (5xx, timeout, connection reset) where
we cannot tell whether Stripe processed the charge. With a payment
intent recorded, Hardcount asks Stripe for it after a short wait. With
none (a parked invoice), Hardcount lists the customer’s payments at
Stripe every billing tick: it records a payment for the invoice that
appears, and if none has appeared 24 hours after the attempt the
invoice goes back to pending (failed after a dunning retry) and
is charged again as usual.
Tax rate as a percent (18.50 = 18.50%), up to 4-decimal precision. ADR-042/043.
Durable audit snapshot of the upstream tax engine that ran the
calculation (e.g. stripe, none, manual).
Provider-side calculation reference captured at build time. For Stripe Tax this expires after 24 h; persisted so finalized invoices remain reconstructable past expiry.
Provider-side committed transaction reference, captured at
finalize. For Stripe Tax this is tx_xxx and is the handle
the provider needs for a later reversal when a credit note is
issued.
Whether tax has been calculated. ok: it has. pending: the
calculation failed, and tax_error_code says why and whether
Hardcount retries it. A pending invoice cannot be finalized, so a wrong
tax amount is never sent.
Why tax could not be calculated, as a code. Set while
tax_status is pending; tax_pending_reason has the
detail.
The earliest time the background retry may calculate tax again, after a failed attempt. Absent when a retry may run now.
When the provider’s tax transaction was reversed after a void or write-off, or found to need no reversal. Absent while the reversal is not confirmed.
Set when a payment arrived that the invoice did not expect. The
webhook event name of the cause: payment.duplicate_charge,
payment.amount_mismatch, payment.currency_mismatch or
payment.received_on_voided_invoice.
Only the latest one is kept.
The Stripe PaymentIntent of that unexpected payment.
The amount Stripe took in that unexpected payment.
Brand of the saved card that paid the invoice. Absent when the card is unknown.
Last four digits of the saved card that paid the invoice.
Unified “this invoice needs operator attention” surface.
Computed server-side from durable invoice fields
(tax_status, tax_error_code, payment_status,
last_payment_error). Never persisted —
always derived. Omitted entirely when the invoice is healthy
(matches Stripe’s last_finalization_error: null ergonomic).
See ADR-009.
object
Urgency of an Attention surface. Operators sort/filter on this and the dashboard renders one colour per level. Priority descending: Critical > Warning > Info.
Closed, typed code naming why an invoice needs operator
attention. Stable public-API contract — codes are never
repurposed; deprecations keep the code reserved and add a
successor. See Attention.code for the open, provider-specific
sub-code. payment_anomaly (ADR-068) reports money that does
not reconcile — double charge, captured-vs-booked amount
mismatch, or a payment on a voided invoice — and is surfaced
even on paid/voided invoices. dunning_exhausted reports a
no-payment-method invoice whose automatic recovery has ended
without collecting (2026-07-22). collection_paused reports an
invoice that is queued for auto-charge but whose subscription
has pause_collection set, so the charge sweeps deliberately
skip it (2026-07-28). A dunning run already open on the invoice
keeps retrying it through the pause, and message then says so;
with no open run it is not charged automatically until
collection resumes. commit_exposure reports an
unpaid invoice that funded a prepaid commit whose credit is
already spendable (ADR-078 funds commits at issue, not at
payment) — note this reason is only returned when no other
reason applies; when one does, the exposure is folded into that
reason’s message and its actions instead, so clients must
not treat the reason code as the presence test for exposure
(2026-08-04). dispute_lost reports a payment on the invoice that
the customer disputed with their bank and that the dispute took
back (the dispute closed as lost). Hardcount leaves the invoice paid
and moves no money; the operator records the loss. Surfaced on paid
invoices. On an invoice that bought prepaid credit, the message
also says how much of the credit is still spendable and names
commit relief, because an ordinary credit note is refused there.
When the invoice also has payment_anomaly, that reason stays and
its message asks the operator to check Stripe before any refund.
The disputes themselves are at GET /v1/invoices/{id}/disputes
(2026-10-19). amount_below_minimum reports an invoice Stripe
refused to charge, or refused a payment page for, because the amount
is below Stripe’s minimum charge. The minimum depends on the
currency the Stripe account settles in; Stripe’s reply, in
provider_response, names it. Nothing was charged, the invoice
stays finalized with payment_status pending, it is not
charged again automatically, dunning does not start (a run already
open ends), and the customer is not emailed. The operator voids it
or records a payment; Collect payment applies the customer’s credit
balance and pays it if that covers the amount, and otherwise asks
Stripe again. Stripe’s answer can change (for a converted charge the
minimum is the settlement currency’s), and a different answer ends
this reason (2026-10-23).
Human-readable copy, safe to render verbatim: one plain
sentence saying what happens and what to do. Facts that must
show up front follow on more lines, separated by a newline
(\n), one sentence each and up to three lines in all: the
setup-link email on no_payment_method (and on
dunning_exhausted when no link can be emailed), and the
prepaid credit at risk on an unpaid commit invoice. A no-card
commit invoice carries all three. Wording may change between
releases; switch on reason and code, not on this text.
Prescribed next steps. First item is the primary CTA; the rest are secondary.
object
Operator’s recommended next step. Closed enum because every
code maps to a concrete server endpoint or frontend route,
and audit logs key off the code. reconcile_payment opens the
payment in Stripe; it is offered on payment_anomaly, where the
refund or correction is made there. mark_uncollectible opens
the write-off and is offered only on a parked invoice (payment
status unknown with no payment intent recorded), for a customer
who paid another way: Hardcount records a payment that Stripe shows,
and charges the invoice again if none appears within 24 hours.
Dashboards render it as a secondary action. void_invoice is offered
on prepaid-commit exposure, only while some of the granted
credit is still unspent: voiding retires the unspent balance
(ADR-078) and is the sole recovery for an unpaid invoice whose
credit is already live. Credit the customer has already spent
is never clawed back (2026-08-04). It is also offered on
amount_below_minimum, an amount Stripe will not charge
(2026-10-23).
Default rendering label. UIs are free to substitute localized copy.
Open, dotted, provider-specific code (e.g.
tax.customer_data_invalid, payment.declined).
New codes ship without a contract
bump. Stripe parity.
Deep link to operator-facing documentation for this reason. Stripe parity.
Dotted-path pointer at the entity field the operator must
edit to resolve the issue (e.g.
customer.address.postal_code). Stripe parity. Empty
when the resolution isn’t field-scoped.
Hardcount’s own explanation behind message: why the state
holds, what Hardcount does on its own, and ids support may ask
for (for example the payment intent ids of a payment
anomaly). Plain text; may contain blank-line paragraph
breaks. Never a provider payload — that is
provider_response.
The provider’s own response (Stripe Tax error envelope, Stripe’s last payment error), exactly as received. Present only when Hardcount called the provider.
When the attention condition started — operators triage by age. tax_deferred_at for tax reasons; due_at for overdue; updated_at as a proxy for payment failures.
When Hardcount will next retry on its own. Today only a tax retry sets it. Absent when no automatic retry is scheduled.
The invoice’s payment deadline (its due_at). Hardcount does not
act on this date; it is shown so the operator knows it.
Absent when the invoice has no due date.
What an operator must know BEFORE recording an OFFLINE payment on a written-off invoice — a consequence that will go unreconciled, NOT a refusal. Absent when there is nothing to say.
object
Always false — this shape only ever warns.
Operator-facing sentence naming the consequence that will go unreconciled if the payment is recorded.
Human period string with the inclusive last covered day (“Jun 1, 2028 – Jun 30, 2028”), date-only in the invoice’s billing timezone (ADR-058 / ADR-077). Computed on read; the raw billing_period_start/end stay half-open. Empty/omitted for one-off invoices with no period.
IANA timezone the period boundaries are anchored in — the org billing timezone resolved and denormalized onto the invoice at issue (ADR-077). billing_period_display is rendered in this zone, so an issued invoice’s dates stay fixed even if the org later changes its timezone. Empty only on legacy invoices created before the zone was stamped (cycle invoices before migration 0134, manual invoices before 2026-10-16).
The buyer details this invoice was issued with (ADR-109), copied when it left draft. The PDF, the hosted page and the dashboard render an issued invoice from this copy, so a later edit to the customer or billing profile does not change it. Absent on drafts and on invoices issued before this field existed; those render from the current customer details.
object
On an invoice, the tax ID its tax was calculated with when set, otherwise the billing profile’s tax ID at issue.
The seller (company) details this invoice was issued with (ADR-109), copied when it left draft. Absent on drafts and on invoices issued before this field existed. Branding (logo, brand color) is not part of it.
object
Set when the invoice transitions out of draft.
When the invoice was marked uncollectible.
The Stripe PaymentIntent of the card charge that paid the
invoice, or of its last charge that is in progress or failed. It
can be absent after a charge was sent: for example when the
invoice was then paid without a card (by credits, with nothing
due, or outside Hardcount: paid_out_of_band), or when Stripe
refused the last charge as below its minimum.
True when the payment was recorded as made outside Hardcount, such as
a bank transfer or cheque (POST /v1/invoices/{id}/record-payment,
or a payment recovery run resolved as payment_recovered). Such
an invoice has no stripe_payment_intent_id, so there is no card
to refund.
When the customer was emailed a link to add a payment method for this invoice. The email is sent once.
Stripe’s PaymentIntent status the last time Hardcount checked it,
word for word. For information only: payment_status is the
invoice’s payment state. Absent until Hardcount has checked. An
invoice checked by the Stripe Search sweep of releases before
this one may still show search_not_found,
search_terminal_only or search_error.
When Hardcount last asked Stripe about this invoice’s payment, whether
or not Stripe answered: a read of provider_payment_status, or a
check of a charge whose result was lost.
Hosted-invoice-URL credential (Stripe-parity
hosted_invoice_url). Generated at finalize; drafts have an
empty token. Rotatable via POST /v1/invoices/{id}/rotate-public-token.
The tenant’s invoice_footer as it was when this invoice was
issued. Printed on the PDF and the hosted invoice page. Empty on
a draft.
object
Classifies per-item proration artifacts so the dedup index can
distinguish a plan-change and a quantity-change that happen to
share the same (subscription, item, timestamp).
Mirrors Stripe’s invoice.billing_reason. Classifies the trigger
that produced the invoice. Stamped at create time and never mutated.
Whether this invoice’s domain timestamps (created/issued/due/paid) were stamped on a test clock’s simulated time rather than wall-clock. Captured at write time (engine: the subscription is clock-pinned; manual composer: the customer is clock-pinned). The dashboard renders a “simulated” badge from this authoritative flag instead of inferring it. Always false in live mode.
The test clock this invoice’s dates follow: the clock its customer
is pinned to, for cycle and one-off invoices alike. Read from the
customer on GET /v1/invoices and GET /v1/invoices/{id}; other
responses may leave it out. Left out for a customer on wall-clock
time, and always in live mode.
How many rows match the request’s filters, on every page, not only this one. The audit log and the customer list’s search and name or email sort say where they stop counting.
Example
{ "data": [ { "id": "hc_inv_42014438d82ddf33", "status": "draft", "payment_status": "pending", "currency": "USD", "tax_status": "ok", "tax_error_code": "customer_data_invalid", "attention": { "severity": "info", "reason": "tax_calculation_failed", "actions": [ { "code": "edit_billing_profile" } ] }, "recovery_warning": { "code": "tax_reversed_unrecoverable" }, "source_change_type": "plan", "billing_reason": "subscription_cycle" } ]}limit or offset is out of range or not a whole number, or the request sends the removed after cursor (param names it)
object
object
One of invalid_request_error, authentication_error,
rate_limit_error or api_error. Every error the API
answers has this shape, including an unknown path (404,
code route_not_found) and a method the path does not take
(405, code method_not_allowed, with an Allow header).
The few exceptions are listed in docs/api-surface.md.
The request field the error is about. Sent only when the error
names one. A request body may hold only the fields its
operation lists, spelled and cased as listed: any other
field, or one the server sets (such as id), is refused
with 422 and param naming it. An operation with no request body lists no fields: send
no body, {} or null. The Stripe and Postmark webhooks and
the LiteLLM callback take their sender’s body as it comes.
Example
{ "error": { "type": "invalid_request_error" }}