---
title: "Invoices"
description: "Bill a named customer with line items: create, edit while draft, send, void, the hosted page at /i/:publicId, and how to tell when an invoice is paid (there is no invoice webhook)"
source: https://docs.thru.la/invoices.md
html: https://docs.thru.la/invoices
index: https://docs.thru.la/llms.txt
---
# Invoices

An invoice bills a named customer for line items. When you send it, it gets a hosted page at `https://thru.la/i/<publicId>` where the customer picks a chain and pays. It suits business-to-business billing. For selling to your own users, a checkout session or a direct payment is usually a better fit.

## Create a draft

```bash
curl -X POST https://api.thru.la/v1/invoices \
  -H "x-api-key: $THRU_API_KEY" -H "content-type: application/json" \
  -d '{"customerName":"Example Corp","customerEmail":"billing@example.com","chain":"bnb","network":"mainnet","token":"USDC","lineItems":[{"description":"Consulting, August","quantity":1,"unitAmount":"1500"}],"memo":"Net 15","enabledChains":["bnb","sui"],"successUrl":"https://example.com/invoices/thanks","cancelUrl":"https://example.com/invoices"}'
```

| Field | Rules |
|---|---|
| `customerName` | Required, up to 160 characters |
| `customerEmail` | Optional, up to 200 characters. thru does not email it; you send the link. |
| `chain`, `token` | Required. The default chain and the token the invoice is paid in (`token` up to 64 characters). See `supported-chains`. |
| `network` | Optional; the default is `mainnet` |
| `lineItems` | Required, at least one `{ description, quantity, unitAmount }`. `description` is up to 200 characters, `quantity` a whole number of 1 or more, and `unitAmount` a decimal string **in token units**. The total must be more than zero. |
| `enabledChains` | Optional, up to 15 chains the customer may choose from, at the same amount |
| `currency` | Optional display label, up to 8 characters; the default is `"USD"`. No conversion. |
| `memo` | Optional, up to 1000 characters |
| `dueAt` | Optional ISO date, displayed only; nothing happens when it passes |
| `successUrl`, `cancelUrl` | Optional http(s) URLs for the links on the hosted page. These links are **not signed**, so do not treat a visit to them as proof of payment. |

The response is the invoice, with `id`, `number` (such as `INV-0001`), `publicId`, `status: "draft"` and `amount` (the total).

## Lifecycle

| Endpoint | Does |
|---|---|
| `PATCH /v1/invoices/:id` | Edits a **draft**: `customerName`, `customerEmail`, `lineItems`, `memo`, `dueAt`, `enabledChains`, `successUrl`, `cancelUrl`. Chain, token and network cannot be changed. |
| `POST /v1/invoices/:id/send` | Moves it to `open`. The hosted page now accepts payment. Send the customer `https://thru.la/i/<publicId>`. |
| `POST /v1/invoices/:id/void` | Moves it to `void`. A paid invoice cannot be voided. |
| `GET /v1/invoices/:id` | One invoice |
| `GET /v1/invoices` | Your newest 200 invoices |

Statuses: `draft` → `open` → `paid`, or `void`.

## Payment

When the customer pays on the hosted page, thru creates **one** payment for the invoice total on the chain they chose (see `payments`). If the customer reloads the page, they get the same payment again. The public endpoints behind the page are `GET /v1/public/invoices/:publicId` and `POST /v1/public/invoices/:publicId/pay`.

That payment expires after 30 minutes like any other, and the invoice keeps pointing at it: the page cannot issue a new address. A transfer that arrives within 7 days of the expiry is still credited automatically, and the invoice becomes `paid` on the next read; a later one is credited by thru support from its transaction hash, and nothing is replayed to make that happen. A transfer that arrives after the invoice was already paid can be credited the same way — it is an accepted case, not a refused one — and although the payment has been paid out by then, the credit re-opens that payout and the next run forwards the rest (a second `settlement.completed`, `sequence: 2`). Only a `refunded` payment is out of scope. `payments` describes the window, the recovery and the payout in full. To bill again after that, void the invoice and create a new one.

## Knowing that an invoice was paid

**There is no invoice webhook.** You get the ordinary `payment.*` events, and the payment carries no invoice id or metadata. Two ways to connect them:

- Read the invoice: `GET /v1/invoices/:id` returns `status: "paid"` once its payment is `confirmed` or `overpaid`, and `paymentId` once the customer has started paying. The status is brought up to date when you read it.
- On `payment.confirmed` or `payment.overpaid`, match `data.payment.id` against the `paymentId` of your open invoices.

An underpaid invoice stays `open`; check its payment's `receivedAmount`.

---

Related: [payments](https://docs.thru.la/payments.md) · [webhooks](https://docs.thru.la/webhooks.md) · [products](https://docs.thru.la/products.md)
