Skip to main content
An invoice is a bill with line items and amounts you supply in the create call. No product setup, no catalogue — which makes it the right tool whenever the amount is decided at request time: account credits, usage top-ups, consulting fees, one-off bills. Every invoice gets a per-store sequential number, its own hosted payment page, and an arbitrary metadata object echoed back in every webhook for it. Amounts, timestamps and ids follow the conventions that hold across the API.

Create an invoice

POST /v1/invoices — requires the invoices:write scope.

Request body

Exact types, bounds and formats for every field are in the API Reference; the bounds that bite are collected on Request limits.
items and templateId are jointly required — supply at least one. A body with neither fails schema validation and comes back as the generic { "error": "Invalid request data" }. When both are present, items replaces the template’s items wholesale rather than merging with them. The same wholesale-replacement rule applies to memo and the expiry.
Each entry in items carries a name, an optional description, a quantity and a unitAmount — a fiat decimal string like "1" or "9.99", never a number. The invoice total is Σ (unitAmount × quantity), and must come out greater than zero.

The Idempotency-Key header

Identical semantics to orders: optional, 1 to 255 characters, unique per store. A retried create with a key this store has used before returns the original invoice instead of minting a second one, whatever the body says. Keys never expire. See Idempotency.

Example

201 Created
Redirect the customer to url. The hosted invoice page shows the line items, the total, your store branding, and the same pick-a-token → locked-quote payment flow as the order checkout.
createdVia is api for invoices your server created and dashboard for ones a human issued. The email object is the live send allowance — see Emailing an invoice.

Errors

All 400 with a { "error": … } body. Anything the request schema rejects never reaches the invoice logic — the validator answers first, always with the generic { "error": "Invalid request data" }. The same item-count and expiry bounds are enforced on templates when they are authored, so resolving items or the expiry from a template cannot smuggle a violation past this either. The messages below are the ones the invoice logic itself produces, once the body’s shape is already known to be valid.

Retrieve an invoice

GET /v1/invoices/{id} — requires invoices:read. {id} is the invoice’s short id. Returns the same shape as the create response plus a transactions array: the on-chain transfers that paid it, across every payment attempt.
200 OK
A well-formed id that does not resolve — undecodable, unknown, or an invoice belonging to another store — answers 404 with { "error": "Invoice not found" }. A malformed id is a 400; see Errors.

List invoices

GET /v1/invoices — requires invoices:read. Newest first.
200 OK

Void an invoice

POST /v1/invoices/{id}/void — requires invoices:write. Marks an open invoice as voided so it can no longer be paid, and fires invoice.voided. Returns 200 with the invoice in its new state.

What a void does to the invoice’s payment attempts

A live attempt — processing, quote not yet lapsed — refuses the call outright with Invoice has a payment in progress — try again after it expires. A buyer mid-payment does not get the invoice pulled out from under them. Past that guard, the void retires every still-watchable attempt first. This is the part worth understanding: the payment indexers select attempts on status alone, so an attempt whose quote window has already lapsed but which no indexer tick has reaped yet is still being watched and its deposit address is still payable. Voiding around such an attempt would leave a live address collecting funds for an invoice that can never be paid, so they are retired as part of the call. If funds had already landed on one of those attempts, retiring it makes the invoice partial — terminal, and resolved manually by you — rather than voided. The call then fails with Only open invoices can be voided, because by the time the void would apply the invoice is no longer open. Re-read the invoice to see the partial status.
Note the status quirk in the table: on this endpoint only, a well-formed id with no matching invoice is reported as 400, not 404 — 404 is reserved for an id that passes the short-id format check but does not decode. Do not branch on the status here; branch on the message. See the two ambiguous endpoints.

Emailing an invoice

POST /v1/invoices/{id}/send mails the invoice to its own stored customerEmail. It is rate-limited at three levels and it is the one endpoint whose error body differs from the rest of the API, so it has its own page: Emailing an invoice. Every invoice payload also carries a live email object — sendCount, sendsRemaining, maxSends, lastSentAt, cooldownUntil, canSend, blockedReason — so you can read the allowance without attempting a send.

Invoice templates

GET /v1/invoice-templates — requires invoices:read. Templates are reusable presets — default items, memo and expiry — authored in the dashboard only. This read exists so your server can discover template ids instead of hard-coding them. The response is the store’s templates, newest first, unpaginated (the set is small and merchant-curated).
200 OK
Pass a row’s id as templateId on POST /v1/invoices. Remember that anything you send explicitly replaces the corresponding template default wholesale.

Statuses

See Payment lifecycle for what happens underneath, and Webhooks for the five invoice.* events.