The invoice object
A Hardcount invoice. Returned from create, finalize, void, list, get, rotate-public-token, and collect-payment endpoints.
Fields
Section titled “Fields”| Field | Type | Always present | What it means |
|---|---|---|---|
id |
string | Yes | |
tenant_id |
string | No | Owning tenant. Returned for diagnostic clarity but redundant with the API key (which already determines the tenant). Omitted when empty. |
customer_id |
string | Yes | |
subscription_id |
string | Yes | Empty for one-off invoices not tied to a subscription. |
invoice_number |
string | Yes | Set when the invoice is created, including drafts. |
status |
enum | Yes | 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). One of: draft, finalized, paid, voided, uncollectible. |
payment_status |
enum | Yes | 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. One of: pending, processing, succeeded, failed, unknown. |
currency |
string | Yes | |
subtotal_cents |
integer (int64) | Yes | |
discount_cents |
integer (int64) | Yes | |
tax_amount_cents |
integer (int64) | Yes | |
tax_rate |
number (double) | Yes | Tax rate as a percent (18.50 = 18.50%), up to 4-decimal precision. ADR-042/043. |
tax_name |
string | No | |
tax_country |
string | No | |
tax_id |
string | No | |
tax_provider |
string | No | Durable audit snapshot of the upstream tax engine that ran the calculation (e.g. stripe, none, manual). |
tax_calculation_id |
string | No | Provider-side calculation reference captured at build time. For Stripe Tax this expires after 24 h; persisted so finalized invoices remain reconstructable past expiry. |
tax_transaction_id |
string | No | 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. |
tax_reverse_charge |
boolean | No | |
tax_exempt_reason |
string | No | |
tax_status |
enum | No | 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. One of: ok, pending. |
tax_deferred_at |
string (date-time) | No | Can be null. |
tax_retry_count |
integer | No | |
tax_pending_reason |
string | No | |
tax_error_code |
enum | No | Why tax could not be calculated, as a code. Set while tax_status is pending; tax_pending_reason has the detail. One of: customer_data_invalid, jurisdiction_unsupported, provider_outage, provider_auth, provider_not_configured, unknown. |
tax_next_retry_at |
string (date-time) | No | The earliest time the background retry may calculate tax again, after a failed attempt. Absent when a retry may run now. |
tax_reversed_at |
string (date-time) | No | 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. |
payment_anomaly_kind |
string | No | 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. |
payment_anomaly_payment_intent_id |
string | No | The Stripe PaymentIntent of that unexpected payment. |
payment_anomaly_captured_cents |
integer (int64) | No | The amount Stripe took in that unexpected payment. |
payment_card_brand |
string | No | Brand of the saved card that paid the invoice. Absent when the card is unknown. |
payment_card_last4 |
string | No | Last four digits of the saved card that paid the invoice. |
attention |
Attention | No | 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. |
recovery_warning |
Recovery warning | No | 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. |
total_amount_cents |
integer (int64) | Yes | |
amount_due_cents |
integer (int64) | Yes | |
amount_paid_cents |
integer (int64) | Yes | |
credits_applied_cents |
integer (int64) | Yes | |
billing_period_start |
string (date-time) | Yes | |
billing_period_end |
string (date-time) | Yes | |
billing_period_display |
string | No | 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. |
billing_timezone |
string | No | 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). |
bill_to_snapshot |
Document bill to | No | 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. |
supplier_snapshot |
Document supplier | No | 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. |
issued_at |
string (date-time) | No | Set when the invoice transitions out of draft. Can be null. |
due_at |
string (date-time) | No | Can be null. |
paid_at |
string (date-time) | No | Can be null. |
voided_at |
string (date-time) | No | Can be null. |
uncollectible_at |
string (date-time) | No | When the invoice was marked uncollectible. |
stripe_payment_intent_id |
string | No | 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. |
paid_out_of_band |
boolean | Yes | 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. |
last_payment_error |
string | No | |
auto_charge_pending |
boolean | No | |
no_pm_notified_at |
string (date-time) | No | When the customer was emailed a link to add a payment method for this invoice. The email is sent once. |
provider_payment_status |
string | No | 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. |
provider_synced_at |
string (date-time) | No | 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. |
public_token |
string | No | 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. |
net_payment_term_days |
integer | Yes | |
memo |
string | No | |
footer |
string | No | 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. |
metadata |
object | No | |
created_at |
string (date-time) | Yes | |
updated_at |
string (date-time) | Yes | |
source_plan_changed_at |
string (date-time) | No | Can be null. |
source_subscription_item_id |
string | No | |
source_change_type |
enum | No | 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). One of: plan, quantity, add, remove. |
billing_reason |
enum | No | Mirrors Stripe’s invoice.billing_reason. Classifies the trigger that produced the invoice. Stamped at create time and never mutated. One of: subscription_cycle, subscription_create, subscription_cancel, subscription_update, manual, threshold. |
is_simulated |
boolean | Yes | 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. |
test_clock_id |
string | No | 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. |
Attention
Section titled “Attention”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.
| Field | Type | Always present | What it means |
|---|---|---|---|
severity |
enum | Yes | Urgency of an Attention surface. Operators sort/filter on this and the dashboard renders one colour per level. Priority descending: Critical > Warning > Info. One of: info, warning, critical. |
reason |
enum | Yes | 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). One of: tax_calculation_failed, tax_location_required, payment_failed, payment_unconfirmed, payment_anomaly, payment_processing, payment_scheduled, awaiting_payment, no_payment_method, dunning_exhausted, collection_paused, commit_exposure, dispute_lost, amount_below_minimum. |
message |
string | Yes | 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. |
actions |
array of Attention action item | No | Prescribed next steps. First item is the primary CTA; the rest are secondary. |
code |
string | No | Open, dotted, provider-specific code (e.g. tax.customer_data_invalid, payment.declined). New codes ship without a contract bump. Stripe parity. |
doc_url |
string | No | Deep link to operator-facing documentation for this reason. Stripe parity. |
param |
string | No | 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. |
detail |
string | No | 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. |
provider_response |
string | No | The provider’s own response (Stripe Tax error envelope, Stripe’s last payment error), exactly as received. Present only when Hardcount called the provider. |
since |
string (date-time) | No | 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. |
next_attempt_at |
string (date-time) | No | When Hardcount will next retry on its own. Today only a tax retry sets it. Absent when no automatic retry is scheduled. |
due_by |
string (date-time) | No | 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. |
Recovery warning
Section titled “Recovery warning”What an operator must know BEFORE recording an OFFLINE payment on a written-off invoice (status = uncollectible) — a consequence that will go unreconciled, never a refusal. The money already arrived, and refusing to record it would only make the books wrong too. A written-off invoice is settled by RECORDING writers only — an offline payment, or Hardcount recording a card charge that Stripe shows succeeded (its webhook, or the check of an unresolved charge). Nothing initiates a card charge against one (ADR-113): recovery of a returned customer runs on normal rails via a fresh recovery invoice. tax_reversed_unrecoverable — the invoice’s tax was reversed with the tax provider and cannot be re-reported automatically; the recorded payment collects tax already reported as not collected. relief_not_reissued — a credit the invoice was owed was never applied, so amount_due_cents is higher than what is owed and the customer may have overpaid.
| Field | Type | Always present | What it means |
|---|---|---|---|
blocked |
boolean | Yes | Always false — this shape only ever warns. |
code |
enum | No | One of: tax_reversed_unrecoverable, relief_not_reissued. |
message |
string | No | Operator-facing sentence naming the consequence that will go unreconciled if the payment is recorded. |
Document bill to
Section titled “Document bill to”What an issued invoice or credit note prints about its buyer, copied when the document was issued (ADR-109). The printed name is legal_name when set, otherwise display_name.
| Field | Type | Always present | What it means |
|---|---|---|---|
display_name |
string | Yes | |
legal_name |
string | No | |
email |
string | No | |
address_line1 |
string | No | |
address_line2 |
string | No | |
city |
string | No | |
state |
string | No | |
postal_code |
string | No | |
country |
string | No | |
tax_id |
string | No | On an invoice, the tax ID its tax was calculated with when set, otherwise the billing profile’s tax ID at issue. |
tax_id_type |
string | No |
Document supplier
Section titled “Document supplier”What an issued invoice or credit note prints about its seller (the tenant’s company), copied when the document was issued (ADR-109).
| Field | Type | Always present | What it means |
|---|---|---|---|
name |
string | No | |
email |
string | No | |
phone |
string | No | |
address_line1 |
string | No | |
address_line2 |
string | No | |
city |
string | No | |
state |
string | No | |
postal_code |
string | No | |
country |
string | No | |
tax_id |
string | No |
Attention action item
Section titled “Attention action item”| Field | Type | Always present | What it means |
|---|---|---|---|
code |
enum | Yes | 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). One of: edit_billing_profile, retry_tax, retry_payment, wait_provider, rotate_api_key, reconcile_payment, review_registration, charge_now, send_reminder, add_payment_method, update_payment_method, connect_tax_provider, void_invoice, mark_uncollectible. |
label |
string | No | Default rendering label. UIs are free to substitute localized copy. |