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
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/:idreturnsstatus: "paid"once its payment isconfirmedoroverpaid, andpaymentIdonce the customer has started paying. The status is brought up to date when you read it. - On
payment.confirmedorpayment.overpaid, matchdata.payment.idagainst thepaymentIdof your open invoices.
An underpaid invoice stays open; check its payment's receivedAmount.