LayerHopDocsLaunch app (opens in a new tab)

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#

  1. Create the refund for a paid session, with the API or from the dashboard. LayerHop records it as pending and returns a quote for the route back to the payer.
  2. Build the transaction with POST /v1/orders and the quote's quoteId, exactly like a swap.
  3. Sign it from the from wallet and broadcast it.
  4. Report it with POST /v1/orders/{quoteId}/submitted. The refund becomes submitted and LayerHop tracks it as an order.
  5. When the route delivers, the refund is completed, the session's refundedAmount grows, and checkout.session.refunded is sent. If the route fails or is refunded by its provider, the refund is failed and 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.

Terminal
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" }'
JSON
{
  "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:

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

JSON
{
  "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