Get invoice with line items
const url = 'http://localhost:8080/v1/invoices/example';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/example \ --header 'Authorization: Bearer <token>'Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Responses
Section titled “Responses”Invoice with its line items.
Response shape for GET /v1/invoices/{id}. The handler returns the
invoice and its line items as separate top-level keys (rather than
nesting line_items inside the invoice) so the client can render
the two arrays independently.
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.
A single line on an invoice.
object
Owning tenant. Omitted when empty.
The meter’s unit (“tokens”, “seconds”, …), stamped on usage lines at write time so invoice surfaces can apply the display-scale convention (token rates read “$3.00 / 1M tokens”). Empty on non-usage lines and on lines written before the 0173 backfill.
The line’s exact quantity as a decimal string, for example “3” or “1.5”. Line amounts stay whole cents.
The unit price in cents at full precision, for example “0.3” for $0.003. On a usage line priced per_unit it is that rate; on any other line it is amount_cents ÷ quantity. Line amounts and totals stay whole cents.
Tax rate as a percent (18.50 = 18.50%), up to 4-decimal precision. ADR-042/043.
Stripe-canonical structured taxability_reason
(standard_rated, reverse_charge, not_collecting,
customer_exempt, product_exempt, zero_rated, etc.).
prepaid_credit_purchase is Hardcount’s own value, on a prepaid
commit purchase line under the manual or Stripe Tax provider:
the line is not taxed, and the usage the credit pays for is
taxed when invoiced. Otherwise empty for non-Stripe providers.
On a usage line, the shape of its price: per_unit, graduated or package. Absent on other lines.
On a usage line, the plan price that billed it. Absent on other lines.
On a usage line, the labels its price takes. Absent for a price that takes every event, and on other lines.
object
object
Marks the line as a prepaid-commit purchase (ADR-078): when the invoice finalizes, a credit block of this many cents is granted to the customer. May differ from the line price — discounted commits sell $10k of credits for $9k. add_on lines on manual invoices only, at most one per invoice. Absent on normal lines.
Expiry of the granted commit block. The credit can be drawn up to and including this instant; a period invoice draws it when its period ends at or before it. The dashboard sends the local midnight after the picked day. Absent/null = the credits never expire (the default).
Example
{ "invoice": { "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" }, "line_items": [ { "line_type": "base_fee" } ]}Invoice not found.
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" }}