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 →

Add a line item to a draft invoice

POST
/v1/invoices/{id}/line-items
curl --request POST \
--url http://localhost:8080/v1/invoices/example/line-items \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "description": "example", "line_type": "base_fee", "quantity": 1, "unit_amount_cents": 1, "commit_granted_cents": 1, "commit_expires_at": "2026-04-15T12:00:00Z" }'

Adds one line to a draft invoice and recomputes its totals. amount_cents is quantity × unit_amount_cents. Only draft invoices accept new lines.

id
required
string
Idempotency-Key
string

Makes the call safe to retry. Send a unique value (a UUID works) and the same value on every retry. For 24 hours, a repeat with the same key, method, path and body gets the first response again, with an Idempotent-Replayed: true header, instead of running again. The error cases are in docs/api-surface.md. The usage routes do not take it: each usage event carries its own idempotency_key.

Media typeapplication/json

A line to add to a draft invoice.

object
description
required
string
line_type

Defaults to add_on.

string
Allowed values: base_fee usage add_on
quantity
required
integer format: int64
>= 1
unit_amount_cents
required
integer format: int64
>= 1
commit_granted_cents

Makes this line a prepaid commit purchase: when the invoice is finalized, the customer gets this many cents of credit. Only on add_on lines of manual invoices, at most one per invoice.

integer format: int64
>= 1 <= 100000000
commit_expires_at

When the granted credit expires. 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. Must be in the future. Needs commit_granted_cents. Absent means it never expires.

string format: date-time

Line item created.

Media typeapplication/json

A single line on an invoice.

object
id
required
string
invoice_id
required
string
tenant_id

Owning tenant. Omitted when empty.

string
line_type
required
string
Allowed values: base_fee usage add_on
meter_id
string
meter_unit

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.

string
description
required
string
quantity
required

The line’s exact quantity as a decimal string, for example “3” or “1.5”. Line amounts stay whole cents.

string
unit_amount_decimal
required

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.

string
amount_cents
required
integer format: int64
tax_rate
required

Tax rate as a percent (18.50 = 18.50%), up to 4-decimal precision. ADR-042/043.

number format: double
tax_amount_cents
required
integer format: int64
tax_jurisdiction
string
tax_code
string
tax_reason

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.

string
total_amount_cents
required
integer format: int64
currency
required
string
pricing_mode

On a usage line, the shape of its price: per_unit, graduated or package. Absent on other lines.

string
price_id

On a usage line, the plan price that billed it. Absent on other lines.

string
dimension_match

On a usage line, the labels its price takes. Absent for a price that takes every event, and on other lines.

object
key
additional properties
string
billing_period_start
string format: date-time
nullable
billing_period_end
string format: date-time
nullable
metadata
object
key
additional properties
any
created_at
required
string format: date-time
commit_granted_cents

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.

integer format: int64
nullable
commit_expires_at

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

string format: date-time
nullable

Example

{
"line_type": "base_fee"
}

Body is not valid JSON.

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"
}
}

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"
}
}

Validation error: the invoice is not a draft, a line field is invalid (for example a quantity of 0, or a second commit line), or the body has an unknown field (param names it; the body must use only the fields listed here).

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"
}
}