Invoices
Bill a customer by email with line items in USDC or USDT. LayerHop emails the invoice, hosts a payment page at layerhop.com/invoice/{id} where the customer pays with any token on any chain, then marks it paid, emails a receipt and sends invoice.paid.
An invoice bills one customer for one or more line items, in the stablecoin and on the chain you want to be paid in. You create it with an API key or in the dashboard (Payments → Invoices), and send it: LayerHop emails the customer from billing@layerhop.com with a link to the hosted invoice page at https://layerhop.com/invoice/{id}. The customer pays from that page with any token on any chain through checkout, and once the delivery to your address is verified on chain the invoice is paid, the customer gets a receipt (email and PDF), and you get an invoice.paid webhook.
Invoices are non-custodial like every LayerHop payment: the customer's wallet sends the funds and the route delivers them straight to your payout address.
How it works#
- Create an invoice: line items, settlement token and chain, your payout address, and the customer's email. It starts as a
draftyou can still edit. - Send it (or create it with
"send": true). The invoice becomesopen, gets its number (INV-0001,INV-0002, … per account and mode), and the customer receives the email. You getinvoice.sent. - The customer pays on
layerhop.com/invoice/{id}. Pressing Pay starts a checkout session for the invoice total (POST /v1/invoices/{id}/pay). If they come back later, a session that is still live (or already processing) is reused; otherwise a fresh one is started. - LayerHop verifies the payment exactly as for any checkout session. The session becomes
paid, the invoice becomespaid, the customer gets a receipt email with a PDF link, and you receivecheckout.session.completedandinvoice.paid.
| Invoice status | Meaning |
|---|---|
draft |
Created, not sent. Editable with PATCH. Not visible to anyone but you. |
open |
Sent and waiting for payment. The hosted page and pay work. Sending again re-sends the email. |
paid |
The checkout session that pays it was verified on chain. The receipt is available. Final. |
void |
You voided it before it was paid. Any open session for it is expired. Final. |
1. Create and send an invoice#
curl -X POST https://api.layerhop.com/v1/invoices \
-H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
-d '{
"customerEmail": "ap@client.example",
"customerName": "Client Ltd",
"merchantName": "Acme Studio",
"lineItems": [
{ "description": "Website redesign", "quantity": 1, "unitAmount": "1200000000" },
{ "description": "Hosting (months)", "quantity": 3, "unitAmount": "25000000" }
],
"settleChain": "base",
"settleToken": "USDC",
"recipient": "0xYourPayoutAddress",
"memo": "Thanks for your business.",
"dueAt": "2026-11-01",
"send": true
}'With the TypeScript SDK:
import { LayerHop } from "@layerhop/sdk";
const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY });
const invoice = await lh.invoices.create({
customerEmail: "ap@client.example",
customerName: "Client Ltd",
merchantName: "Acme Studio",
lineItems: [
{ description: "Website redesign", quantity: 1, unitAmount: "1200000000" }, // 1,200 USDC
{ description: "Hosting (months)", quantity: 3, unitAmount: "25000000" }, // 3 × 25 USDC
],
settleChain: "base",
settleToken: "USDC",
recipient: "0xYourPayoutAddress",
dueAt: "2026-11-01",
});
await lh.invoices.send(invoice.id); // draft → open, emails the customer
console.log(invoice.url); // https://layerhop.com/invoice/inv_…With the Python SDK:
import os
from layerhop import LayerHop
lh = LayerHop(api_key=os.environ["LAYERHOP_API_KEY"])
invoice = lh.invoices.create(
customer_email="ap@client.example",
line_items=[{"description": "Website redesign", "quantity": 1, "unitAmount": "1200000000"}],
settle_chain="base", settle_token="USDC", recipient="0xYourPayoutAddress",
send=True,
)| Field | Notes |
|---|---|
lineItems |
1 to 50 items of { description, quantity, unitAmount }. quantity is 1 to 10,000; unitAmount is an integer string in base units of the settlement token. The invoice amount is the sum of quantity × unitAmount. |
settleChain, settleToken, recipient |
Where you are paid, with the same supported pairs as checkout. |
customerEmail, customerName |
Who the invoice is for. Without an email, sending opens the invoice but nothing is emailed; share its url yourself. |
merchantName |
Your name as shown on the email, the hosted page and the receipt. |
memo |
A note to the customer (up to 1,000 characters). |
dueAt |
A date (2026-11-01, end of that day UTC) or a timestamp. Shown to the customer; an overdue invoice can still be paid. |
send |
true to send right away instead of keeping a draft. |
The response is the invoice: id (inv_…, or inv_test_… in test mode), number, status, amount, settlement (chainId, token, symbol, decimals), url (the hosted page), receiptUrl (once paid), sessionId (the latest checkout session), and the timestamps sentAt, paidAt, receiptSentAt, voidedAt, createdAt and updatedAt.
2. Manage invoices#
| Action | API | SDK |
|---|---|---|
List (newest first, ?status=draft|open|paid|void, limit, cursor) |
GET /v1/invoices |
lh.invoices.list({ status }) |
| Get one | GET /v1/invoices/{id} |
lh.invoices.get(id) |
Edit a draft (any create field except send) |
PATCH /v1/invoices/{id} |
lh.invoices.update(id, { … }) |
| Send, or re-send the email of an open invoice | POST /v1/invoices/{id}/send |
lh.invoices.send(id) |
| Void (draft or open) | POST /v1/invoices/{id}/void |
lh.invoices.void(id) |
Only drafts can be edited (CONFLICT otherwise): to change a sent invoice, void it and create a new one. Paid and void invoices cannot be sent or voided. The dashboard does the same under Payments → Invoices (it calls the /v1/dashboard/invoices mirror of these endpoints with your sign-in session).
3. The hosted invoice page#
https://layerhop.com/invoice/{id} shows your name, the invoice number, line items, total, memo and due date, and a Pay button that opens the checkout for the total. After paying, the customer returns to the invoice, which now shows Paid and a link to the receipt. A draft is never shown: its page and public endpoints answer NOT_FOUND until you send it.
The page uses two public endpoints (no key needed; the unguessable invoice id is the access token) that you can also call to build your own page:
GET /v1/invoices/{id}/publicreturns the customer's view of the invoice: no customer email, payout address or session id. SDK:lh.invoices.publicView(id).POST /v1/invoices/{id}/payreturns{ sessionId, url }for the checkout session that pays it (201when a new session was started,200when a live one was reused). SDK:lh.invoices.pay(id).
const { url } = await lh.invoices.pay("inv_01k7…");
// send the payer to url (the hosted checkout), or open it with the embedded checkoutThe checkout session for an invoice has invoiceId set, description: "Invoice INV-0001", reference set to the invoice number, and metadata.invoiceId.
4. Emails and receipts#
LayerHop sends two emails from billing@layerhop.com, both in your merchantName:
- The invoice, when you send (or re-send) it: line items, total, settlement token and chain, memo, due date and the Pay invoice link.
- The receipt, once the invoice is paid: the total, when it was paid, what the customer paid with, the transaction hash and a link to the PDF.
receiptSentAtrecords when it went out.
The PDF receipt is at GET /v1/invoices/{id}/receipt.pdf (public, also linked as receiptUrl on the invoice). It lists the line items, the total paid, the settlement token and chain, the paying chain and the transaction hash. Before the invoice is paid it answers NOT_FOUND.
Test-mode invoices are emailed for real, marked Test in the subject and on the PDF, so you can check the customer's experience.
5. Webhooks#
Subscribe a webhook endpoint to:
| Event | Sent when | data |
|---|---|---|
invoice.sent |
You send or re-send an invoice. | invoice (the full invoice) |
invoice.paid |
The checkout session paying it is paid. |
invoice and session (the paid checkout session) |
{
"id": "evt_…",
"type": "invoice.paid",
"livemode": true,
"createdAt": "2026-10-18T14:21:07.000Z",
"data": {
"invoice": {
"id": "inv_01k7v3q8e2h4m6p8r0t2w4y6a",
"object": "invoice",
"number": "INV-0007",
"status": "paid",
"amount": "1275000000",
"settlement": { "chainId": 8453, "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 },
"customerEmail": "ap@client.example",
"customerName": "Client Ltd",
"sessionId": "cs_01k7v3r…",
"paidAt": "2026-10-18T14:21:07.000Z"
},
"session": { "id": "cs_01k7v3r…", "status": "paid", "invoiceId": "inv_01k7v3q8e2h4m6p8r0t2w4y6a", "…": "…" }
}
}You also receive the usual checkout.session.* events for the session that pays the invoice; match them to the invoice with invoiceId.
Test mode#
Create invoices with an lh_test_ key, or with Test mode on in the dashboard. Test invoices (inv_test_…, numbered separately from live ones) are paid through the sandbox: the checkout page shows a Test mode badge, nothing moves on chain, and the invoice is paid about 10 to 20 seconds after the payment is reported. Webhooks go to your test endpoints only.
Last updated