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