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.

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.
Each entry in 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
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 Email an invoice.

Errors

All 400 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
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
An id that is not a well-formed short id fails the route’s validation and answers 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 404404 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:
  • code is one of not_found, not_sendable, rate_limited, send_failed.
  • retryAfterSeconds is always present, and null when the block is not a cooldown you can wait out (an exhausted per-invoice allowance, for example).
  • When retryAfterSeconds is set, the same value is mirrored in a Retry-After header.
A request rejected before it reaches the mailer answers with the plain { "error": … } shape instead, with no code and no retryAfterSeconds: a malformed {id} is 400 { "error": "Invalid request data" }, and a well-formed id that does not decode is 404 { "error": "Invoice not found" }. Read code defensively.
The allowance is also readable without attempting a send: every invoice payload carries an email object.
Check email.canSend before calling the send endpoint rather than treating a 429 as your signal. It costs nothing — the field is on the invoice you already have.

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.