LayerHopDocsLaunch app (opens in a new tab)

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#

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

Terminal
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:

TypeScript
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:

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.
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}/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).
TypeScript
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 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: 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