number, its own hosted payment page, and an
arbitrary metadata object echoed back in every webhook for it.
Create an invoice
POST /v1/invoices — requires the invoices:write scope.
Request body
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:
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.
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
Email an invoice.Errors
All400 with a { "error": … } body.
Anything the request schema rejects — neither items nor templateId, an empty
items, more than 100 line items, expiresIn outside 10 to 129,600, a unitAmount
that is not a decimal string of at most 3 places, a name over 255 characters — never
reaches the invoice logic. The validator answers first, always with the same generic
body:
400 Bad Request
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
400 with { "error": "Invalid request data" }. An id that is well-formed but does
not resolve — undecodable, unknown, or an invoice belonging to another store — answers
404 with { "error": "Invoice not found" }.
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.Email an invoice
POST /v1/invoices/{id}/send — requires invoices:write. Takes no request body.
The recipient is always the invoice’s own stored customerEmail and cannot be
overridden. That is what stops the endpoint from being an open relay. Creating an
invoice never emails anyone; this call is the only thing that does.
200 OK
The rate-limit envelope
This is the one endpoint whose error body differs from the rest of the API. Every failure the mailer produces carries three fields:codeis one ofnot_found,not_sendable,rate_limited,send_failed.retryAfterSecondsis always present, andnullwhen the block is not a cooldown you can wait out (an exhausted per-invoice allowance, for example).- When
retryAfterSecondsis set, the same value is mirrored in aRetry-Afterheader.
The allowance is also readable without attempting a send: every invoice payload carries
an
email object.
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.