Skip to content
Hardcount
Run it locally
Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

Get the invoice activity timeline

GET
/v1/invoices/{id}/payment-timeline
curl --request GET \
--url http://localhost:8080/v1/invoices/example/payment-timeline \
--header 'Authorization: Bearer <token>'

Returns everything that happened to an invoice, oldest first: lifecycle steps (created, finalized, paid, voided, marked uncollectible), charge attempts (source payment, event_type charge_attempt.succeeded|failed|unknown|below_minimum), dunning retries, credit notes, customer emails, and card disputes (source dispute, a payment.dispute_opened row and, once Stripe closes the dispute, a payment.dispute_closed row; both at Stripe’s own time). A draft invoice returns an empty list.

Each row carries line, the row in the activity line standard’s words (ADR-128). description, detail, error, status and amount_cents repeat it for clients that read the older fields.

If one source cannot be read, the rest is still returned and the source is named in degraded.

id
required
string

Timeline events.

Media typeapplication/json
object
events
required
Array<object>
object
id
required

Stable key for the row.

string
timestamp
required
string format: date-time
source
required
string
Allowed values: lifecycle payment dunning email credit_note dispute
event_type
required
string
status
required

The line’s tone in older words, read by the dashboard’s dot.

string
Allowed values: neutral succeeded warning failed
description
required

The line’s title.

string
detail

The line’s meta line as text, its actor last, for example “Visa •••• 4242 · After 2 retries”.

string
error

The line’s problem.

string
amount_cents
integer format: int64
currency
string
payment_intent_id
string
attempt_count
integer
is_simulated

True when timestamp is test-clock time.

boolean
recorded_at

Real-world time of a row whose timestamp is test-clock time.

string format: date-time
line
required

One activity row in the words of the activity line standard (ADR-128): a title with no values, the amount when the event moves money, asks for it or cancels money owed, a problem line on failure and warning rows, the meta line’s parts and the actor, a tone, and the event’s time on the entity’s calendar. recorded_at is the real-world time, set only for an entity on a test clock.

object
id
required

The row’s stable key: the event’s id on an invoice timeline row, the audit entry’s id on a subscription timeline row, dunning: and the event’s id on a dunning run event (the same key the invoice timeline gives that event), and email: and the email’s id on a sent email (likewise).

string
kind
required

Machine name of the event.

string
title
required
string
amount
object
cents
required
integer format: int64
currency
required
string
problem
string
detail
required

The meta line before the actor. Each item is one segment, shown joined by “ · “; a segment’s parts read as one phrase, for example a money part followed by the text “ refunded to card”.

Array<Array<object>>
actor

Who did it. person and api_key carry the name (a person’s email when they have no name), customer says where through via, and automatic is anything Hardcount did on its own. Absent when no fact written with the row names one.

object
kind
required
string
Allowed values: person api_key customer automatic
name
string
via
string
Allowed values: payment_page portal
tone
required

Neutral: something happened. success: money arrived, or was returned as intended. warning: needs a look, or money will not be collected. failure: an attempt failed.

string
Allowed values: neutral success warning failure
occurred_at
required
string format: date-time
recorded_at
string format: date-time
degraded
required

Sources that could not be read, so their rows are missing.

Array<string>
Allowed values: audit credit_notes emails dunning charge_attempts disputes
truncated
required

True when the credit-note source hit its limit of 100 rows.

boolean

Example

{
"events": [
{
"source": "lifecycle",
"event_type": "invoice.finalized",
"status": "neutral",
"line": {
"kind": "invoice.voided",
"title": "Payment failed",
"amount": {
"currency": "USD"
},
"problem": "Card declined: insufficient funds",
"detail": [
[
{
"money": {
"currency": "USD"
},
"link": {
"kind": "invoice"
}
}
]
],
"actor": {
"kind": "person",
"via": "payment_page"
},
"tone": "neutral"
}
}
],
"degraded": [
"audit"
]
}

Invoice not found.

Media typeapplication/json
object
error
object
type

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.

string
code
string
message
string
request_id
string
param

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.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}