# Checkout (pay with any token)

> Accept crypto payments in USDC or USDT while your customers pay with any token on any of 70 chains. Create a checkout session, send the payer to the hosted page, and get a checkout.session.completed webhook once LayerHop has verified the delivery on chain.

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

Checkout lets you get paid in a stablecoin you choose (USDC or USDT on Ethereum, Arbitrum, Base, Optimism, BNB Chain, Polygon or Solana) while your customer pays with **any token on any chain** LayerHop routes. You create a checkout session for an amount, send the customer to its hosted page, and LayerHop routes their payment so that at least the amount due lands in your wallet. When the delivery is verified on chain, the session becomes `paid` and you receive a `checkout.session.completed` webhook.

LayerHop never holds the funds: the payer's wallet sends them, and the route delivers straight to your address.

## How it works

1. **Your server** creates a session with an [API key](/guides/dashboard-and-api-keys): amount, settlement token and chain, your payout address. You get back a hosted `url` (`https://layerhop.com/pay/cs_…`). You can also create payment links by hand in the [dashboard](https://dashboard.layerhop.com) under **Payments**.
2. **Your customer** opens the link, connects an EVM or Solana wallet and picks what to pay with. The page asks LayerHop for a route that delivers the full amount to your address and shows exactly what they pay.
3. **The payer signs** one transaction (plus an approval for some ERC-20 tokens). The session moves to `processing`.
4. **LayerHop tracks the payment** and, once it has landed, checks the settlement chain for a transfer of at least the amount due to your address. Only then is the session `paid` and the webhook sent. The page sends the customer back to your `successUrl`.

| Session status | Meaning |
|---|---|
| `open` | Waiting for payment. A failed or refunded payment attempt returns the session here so the customer can try again. |
| `processing` | A payment was sent and is being tracked. |
| `paid` | Delivery to your address was verified on the settlement chain. Fulfil the order. |
| `expired` | Not paid before `expiresAt` (default 60 minutes), or you expired it. |
| `needs_review` | The payment completed but the delivery could not be verified automatically. Rare; contact dev@layerhop.com with the session id. |

## 1. Create a session

```bash
curl -X POST https://api.layerhop.com/v1/checkout/sessions \
  -H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
  -d '{
    "amount": "49000000",
    "settleChain": "base",
    "settleToken": "USDC",
    "recipient": "0xYourPayoutAddress",
    "description": "Pro plan, 1 month",
    "reference": "order_1042",
    "successUrl": "https://shop.example.com/thanks",
    "cancelUrl": "https://shop.example.com/cart"
  }'
```

With the TypeScript SDK:

```ts
import { LayerHop } from "@layerhop/sdk";

const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY });
const session = await lh.checkout.create({
  amount: "49000000", // 49 USDC (6 decimals)
  settleChain: "base",
  settleToken: "USDC",
  recipient: "0xYourPayoutAddress",
  description: "Pro plan, 1 month",
  reference: "order_1042",
  successUrl: "https://shop.example.com/thanks",
});
// Redirect the customer to session.url
```

| Field | Notes |
|---|---|
| `amount` | Integer string in base units of the settlement token (USDC and USDT have 6 decimals, except 18 on BNB Chain). |
| `settleChain`, `settleToken` | One of the supported pairs: USDC on Ethereum, Arbitrum, Base, Optimism, BNB Chain, Polygon or Solana; USDT on Ethereum, Arbitrum, BNB Chain or Solana. |
| `recipient` | Your payout address on the settlement chain. LayerHop sets it on every route; the payer cannot change it. |
| `description`, `merchantName` | Shown to the payer. |
| `reference`, `metadata` | For your records. Returned to you and in webhooks, never shown to the payer. |
| `successUrl`, `cancelUrl` | Where the hosted page sends the payer. `?session_id=cs_…` is appended to `successUrl`. |
| `expiresInMinutes` | 5 to 10080 (7 days). Default 60. |

## 2. Send the customer to the hosted page

Redirect to `session.url`, link to it, or show it as a QR code. To keep customers on your site, open the session in a popup or inline with the [embedded checkout](/guides/embedded-checkout) instead. For a fixed price with no backend, use a [product and payment button](/guides/payment-buttons); for WooCommerce, the [WordPress plugin](/guides/wordpress). The page works on desktop and mobile with any EVM or Solana wallet, shows the amount due and the expiry, and walks the payer through approval, signing and confirmation. Sessions can be paid once.

## 3. Fulfil on the webhook

Add a [webhook endpoint](/guides/webhooks) subscribed to `checkout.session.completed` (or to all events) and verify the signature as usual. The event carries the session:

```json
{
  "id": "evt_…",
  "type": "checkout.session.completed",
  "livemode": true,
  "createdAt": "2026-10-18T10:03:12.000Z",
  "data": {
    "session": {
      "id": "cs_01k7t9m2c4x8q6r0v3b5n7p9d",
      "status": "paid",
      "amount": "49000000",
      "settlement": { "chainId": 8453, "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "symbol": "USDC", "decimals": 6 },
      "recipient": "0xyourpayoutaddress",
      "reference": "order_1042",
      "payment": { "payer": "0x…", "srcChainId": 42161, "srcToken": "0x0000000000000000000000000000000000000000", "srcAmount": "15623001000000000", "srcTxHash": "0x…", "dstTxHash": "0x…", "paidAt": "2026-10-18T10:03:12.000Z" }
    }
  }
}
```

Match the session to your order with `reference` (or the session `id`) and fulfil it. The other checkout events are `checkout.session.processing` (a payment was sent), `checkout.session.payment_failed` (the attempt failed or was refunded; the session is open again), `checkout.session.expired`, and `checkout.session.refunded` (you [refunded](/guides/refunds) part or all of a paid session). You can also poll [`GET /v1/checkout/sessions/{id}`](/api-reference#get-a-checkout-session).

> [!TIP]
> Treat `paid` as final. Do not fulfil on `processing`: a cross-chain payment can still be refunded.

Sessions also carry:

| Field | Meaning |
|---|---|
| `refundedAmount` | Total of completed [refunds](/guides/refunds), in settlement-token base units (`"0"` until you refund). The session stays `paid` after a refund. |
| `invoiceId` | The [invoice](/guides/invoices) this session pays, or `null`. |
| `productId` | The [product](/guides/payment-buttons) the session was started from, or `null`. |

## Invoices, refunds and analytics

- **[Invoices](/guides/invoices)**: bill a customer by email with line items. LayerHop hosts the invoice at `layerhop.com/invoice/{id}`, starts a checkout session when they pay, and emails a receipt.
- **[Refunds](/guides/refunds)**: return all or part of a paid session to the payer's original chain and token. You sign the refund from your own wallet; LayerHop routes and tracks it.
- **[Analytics and CSV export](/guides/analytics)**: revenue, conversion and refunds in the dashboard, and every session as a CSV for your accounting.

## What the payer pays

- **Same token, same chain** (for example USDC on Base for a USDC-on-Base session): a plain token transfer of exactly the amount, with no LayerHop fee.
- **Anything else**: a swap or bridge route through LayerHop's providers. The route guarantees at least the amount due after slippage, so the payer sends slightly more than the face value (usually under 1%, capped at about 3%), and you may receive a little extra. The standard LayerHop API fee (0.20%) and the provider's fees are included in what the payer sees before signing.

## How LayerHop verifies payments

A session is only marked `paid` after LayerHop checks the settlement chain:

- the delivering transaction succeeded and contains a transfer of the settlement token to your `recipient` totalling at least `amount` (EVM: `Transfer` logs; Solana: the token balance change of your account);
- it was mined after the session was created;
- for a direct transfer, it came from the payer;
- it has not already paid another session (a transaction can pay only one session).

A link cannot be paid from its own payout address: a transfer from your wallet to itself moves nothing, so LayerHop refuses to quote it (`INVALID_PARAMS`). To try a live link yourself, pay from a different wallet.

If any check fails the session goes to `needs_review` instead of `paid`.

## Test mode

Create sessions with an `lh_test_` key (or with **Test mode** on in the dashboard). Test sessions (`cs_test_…`) are paid through the [sandbox](/guides/test-mode): the hosted page shows a **Test mode** badge, nothing moves on chain, and the session is `paid` about 10 to 20 seconds after the payment is reported. Webhooks go to your test endpoints only. Use the [magic amounts](/guides/test-mode#magic-amounts) to rehearse a failed or refunded payment.

## Build your own payment page

The hosted page uses public endpoints you can call yourself:

1. [`GET /v1/checkout/sessions/{id}`](/api-reference#get-a-checkout-session) to show the amount and status (no key needed; `reference` and `metadata` are omitted).
2. [`POST /v1/checkout/sessions/{id}/quote`](/api-reference#quote-a-payment-for-a-session) with `{ srcChain, srcToken, sender }`. Every quote's `dstAmountMin` is at least the amount due, and `srcAmount` is what the payer sends.
3. [`POST /v1/orders`](/api-reference#build-the-transaction) with the `quoteId`, sign the transaction in the payer's wallet, then [`POST /v1/orders/{quoteId}/submitted`](/api-reference#report-the-broadcast-transaction) with the hash.
4. Poll the session until it is `paid`.

With the SDK: `lh.checkout.get(id)`, `lh.checkout.quote(id, { srcChain, srcToken, sender })`, `lh.buildTx(quoteId)`, `lh.reportSubmitted(quoteId, txHash)`.
