Refunds
Refund a paid checkout session, fully or in part. Refunds are non-custodial — you sign them from your own wallet — and LayerHop routes the settlement token back to the payer's original chain and token, then sends checkout.session.refunded.
A refund sends money back to the customer who paid a checkout session. LayerHop never holds your funds, so it cannot refund for you: you sign the refund from your own wallet (by default the session's payout address, which received the payment). What LayerHop does is the routing: it quotes the way back from your settlement token to the payer's original chain and token (or the settlement token itself, if you prefer), builds the transaction, tracks it, and when it lands adds it to the session's refundedAmount and sends a checkout.session.refunded webhook.
You can refund the whole amount or part of it, in as many refunds as you like, up to the amount paid. LayerHop takes no fee on refunds; only the route's own provider fees and gas apply.
How it works#
- Create the refund for a
paidsession, with the API or from the dashboard. LayerHop records it aspendingand returns a quote for the route back to the payer. - Build the transaction with
POST /v1/ordersand the quote'squoteId, exactly like a swap. - Sign it from the
fromwallet and broadcast it. - Report it with
POST /v1/orders/{quoteId}/submitted. The refund becomessubmittedand LayerHop tracks it as an order. - When the route delivers, the refund is
completed, the session'srefundedAmountgrows, andcheckout.session.refundedis sent. If the route fails or is refunded by its provider, the refund isfailedand the amount can be refunded again.
| Refund status | Meaning |
|---|---|
pending |
Created and quoted; waiting for you to sign and report the transaction. Does not count against the refundable amount. |
submitted |
The transaction was reported and is being tracked. |
completed |
Delivered to the payer. Counted in the session's refundedAmount. |
failed |
The refund transaction failed. Nothing was refunded; you can create a new refund. |
Where does the refund go?#
to |
The payer receives | Notes |
|---|---|---|
original (default) |
The token they paid with, on the chain they paid from, at the address they paid from | A swap or bridge route from your settlement token, through LayerHop's providers. When the payer paid with the settlement token itself, this is a plain transfer. |
settlement |
Your settlement token, on the settlement chain, at the payer's address | A plain transfer when the chain matches. Only when the payer's address can receive on that chain (EVM to EVM, or Solana to Solana). |
The payer's address is the one recorded when they paid (payment.payer on the session). You cannot send a refund elsewhere.
amount is in base units of the settlement token and is what leaves your wallet. For an original refund through a swap or bridge, the payer receives the quote's dstAmount in their token (at least dstAmountMin).
Refund with the API#
Create the refund with your API key. Omit amount to refund everything not yet refunded.
curl -X POST https://api.layerhop.com/v1/checkout/sessions/cs_01k7t9m2c4x8q6r0v3b5n7p9d/refunds \
-H "x-api-key: $LAYERHOP_API_KEY" -H "content-type: application/json" \
-d '{ "amount": "20000000", "reason": "Partial refund: one seat cancelled" }'{
"refund": {
"id": "re_01k7w2…",
"object": "refund",
"sessionId": "cs_01k7t9m2c4x8q6r0v3b5n7p9d",
"livemode": true,
"status": "pending",
"statusDetail": null,
"amount": "20000000",
"reason": "Partial refund: one seat cancelled",
"quoteId": "q_01k7w2…",
"to": { "chainId": 42161, "token": "0x0000000000000000000000000000000000000000", "address": "0xPayer…" },
"txHash": null,
"createdAt": "2026-10-18T15:02:11.000Z",
"updatedAt": "2026-10-18T15:02:11.000Z"
},
"quote": {
"quoteId": "q_01k7w2…",
"provider": "relay",
"srcAmount": "20000000",
"dstAmount": "6371190000000000",
"dstAmountMin": "6307478100000000",
"estimatedSeconds": 12,
"expiresAt": "2026-10-18T15:04:11.000Z",
"from": "0xYourPayoutAddress"
},
"next": "Build the transaction with POST /v1/orders { quoteId }, sign it from the `from` wallet, then report it with POST /v1/orders/{quoteId}/submitted."
}| Field | Notes |
|---|---|
amount |
Settlement-token base units. Default: everything still refundable (paid amount minus completed and in-flight refunds). |
reason |
For your records (up to 500 characters). Returned on the refund and in the webhook. |
from |
The wallet that signs and sends the refund. Default: the session's recipient (your payout address). |
to |
original (default) or settlement. See Where does the refund go? |
Then build, sign and report, as for any LayerHop order. With the TypeScript SDK:
import { LayerHop } from "@layerhop/sdk";
const lh = new LayerHop({ apiKey: process.env.LAYERHOP_API_KEY });
// 1. Create the refund and get its route
const { refund, quote } = await lh.checkout.refunds.create("cs_01k7t9m2c4x8q6r0v3b5n7p9d", {
amount: "20000000", // 20 USDC; omit for a full refund
reason: "Partial refund: one seat cancelled",
});
// 2. Build the transaction for the `from` wallet
const tx = await lh.buildTx(quote.quoteId);
// 3. Sign and send it from quote.from (your payout wallet), e.g. with viem:
const hash = await walletClient.sendTransaction({ to: tx.to, data: tx.data, value: BigInt(tx.value) });
// 4. Report it so LayerHop tracks the refund
await lh.reportSubmitted(quote.quoteId, hash);
// Later: list the session's refunds
const refunds = await lh.checkout.refunds.list("cs_01k7t9m2c4x8q6r0v3b5n7p9d");If the built transaction needs an ERC-20 approval, it comes with an approval like any other order; see Approvals. With Python: lh.checkout.refunds.create(session_id, amount="20000000", reason="…") and lh.checkout.refunds.list(session_id).
Note
The quote expires like any LayerHop quote. If it expires before you build the transaction, create a new refund; a
pendingrefund that is never reported does not reduce the refundable amount.
Errors you may see: CONFLICT when the session is not paid, has no recorded payer, or has nothing left to refund; INVALID_PARAMS when amount exceeds what is still refundable or to: "settlement" is impossible for the payer's chain; NO_ROUTE when no provider can route back to the payer's token (try to: "settlement" or a different amount).
Refund from the dashboard#
In the dashboard, open Payments, pick a paid session and choose Refund. Enter the amount (full by default) and a reason, connect the wallet that received the payment, and sign. The dashboard creates the refund, builds the transaction, asks your wallet to sign it and reports it for you; the session then shows the refund's status and, once it completes, the refunded amount. (The dashboard uses POST /v1/dashboard/checkout/sessions/{id}/refunds with your sign-in session.)
The session after a refund#
The session stays paid: a refund does not undo the payment, it records money returned. Its refundedAmount (settlement-token base units, "0" until a refund completes) is the total of completed refunds. A session with refundedAmount equal to amount is fully refunded.
Webhook#
checkout.session.refunded is sent each time a refund completes, with the updated session and the refund:
{
"id": "evt_…",
"type": "checkout.session.refunded",
"livemode": true,
"createdAt": "2026-10-18T15:03:40.000Z",
"data": {
"session": { "id": "cs_01k7t9m2c4x8q6r0v3b5n7p9d", "status": "paid", "amount": "49000000", "refundedAmount": "20000000", "…": "…" },
"refund": {
"id": "re_01k7w2…",
"object": "refund",
"amount": "20000000",
"status": "completed",
"reason": "Partial refund: one seat cancelled",
"txHash": "0x…",
"to": { "chainId": 42161, "token": "0x0000000000000000000000000000000000000000", "address": "0xPayer…" },
"createdAt": "2026-10-18T15:02:11.000Z"
}
}
}The refund's order also sends the usual order.* events (it carries refundId).
Test mode#
Refunds of test sessions (cs_test_…) are routed by the sandbox: the built transaction is a placeholder (tx.sandbox: true) that moves nothing, and once you report any well-formed transaction hash for it the refund completes about 10 seconds later and sends checkout.session.refunded to your test endpoints. Refund ids are re_test_….
Last updated