# 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.

Source: https://docs.layerhop.com/guides/invoices

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](/guides/dashboard-and-api-keys) or in the [dashboard](https://dashboard.layerhop.com) (**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](/guides/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](/guides/webhooks).

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

1. **Create** an invoice: line items, settlement token and chain, your payout address, and the customer's email. It starts as a `draft` you can still edit.
2. **Send** it (or create it with `"send": true`). The invoice becomes `open`, gets its number (`INV-0001`, `INV-0002`, … per account and mode), and the customer receives the email. You get `invoice.sent`.
3. **The customer pays** on `layerhop.com/invoice/{id}`. Pressing **Pay** starts a [checkout session](/guides/checkout) 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.
4. **LayerHop verifies the payment** exactly as for any checkout session. The session becomes `paid`, the invoice becomes `paid`, the customer gets a receipt email with a PDF link, and you receive `checkout.session.completed` and `invoice.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

```bash
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:

```ts
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](/sdks/python):

```python
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](/guides/checkout#1-create-a-session). |
| `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](/guides/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}/public` returns the customer's view of the invoice: no customer email, payout address or session id. SDK: `lh.invoices.publicView(id)`.
- `POST /v1/invoices/{id}/pay` returns `{ sessionId, url }` for the checkout session that pays it (`201` when a new session was started, `200` when a live one was reused). SDK: `lh.invoices.pay(id)`.

```ts
const { url } = await lh.invoices.pay("inv_01k7…");
// send the payer to url (the hosted checkout), or open it with the embedded checkout
```

The 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. `receiptSentAt` records 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](/guides/webhooks) 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) |

```json
{
  "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](/guides/test-mode): 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.
