Across Docs

Refunds

What happens to a transfer that does not complete, where the funds go, and how to track it.

Most transfers fill in seconds. When one does not, what happens next depends on the route, and the quote tells you before the user signs.

Before the user signs

refundPolicy

Every quote carries a refundPolicy, discriminated by supported:

// The order carries a refund
{
  "supported": true,
  "token": { "chainId": 8453, "address": "0x8335…" },
  "refundAddress": {
    "address": "0xYourAddress",
    "chain": { "chainId": 8453, "name": "base", "displayName": "Base" /* , … */ }
  },
  "expectedRefundTimeMs": 1800000
}

// The route has no refund step
{
  "supported": false,
  "reason": "irreversible_rail",
  "message": "This route does not guarantee an automatic refund. …"
}

supported: false with reason: "irreversible_rail" means the transfer moves over a rail that has no concept of a refund, such as CCTP. Once the deposit is made, the funds always go through to the destination, so there is no refund for you to handle on these routes. Treat a reason you don't recognise the same way.

When supported is true, token and refundAddress are where a refund would be paid, and expectedRefundTimeMs is an indicative estimate, measured from originValidUntil.

refundTo

refundTo lists every token, chain and address a refund could arrive in. It is advisory, because the route is only fixed after the quote. It is empty when no refund is possible, for example on a same-chain swap.

destination.refundAddress

Set destination.refundAddress on the quote request to choose who receives a refund paid on the destination chain. It defaults to the recipient. Set it when the recipient cannot handle the bridged token, such as a contract that only accepts the final token of a destination swap or action.

It is accepted on EVM destinations only (HyperCore and Lighter included). A Solana or Tron destination returns 400.

When a refund happens

Refunds run automatically: the user signs nothing and pays no gas for them, and Across adds no fee.

A destination refund pays the bridged token. If a destination swap or destinationActions step cannot complete, the refund skips it and pays out the token that crossed, for example USDC on the destination chain when the user wanted WETH. Set destination.refundAddress if the recipient cannot receive that token.

Until a refund executes, a late delivery can still complete the transfer instead. Whichever lands on chain first decides the outcome.

Tracking a refund

Refunds appear on the same transfer you were already tracking, at GET /v1/transfers/{orderId}, and in your webhooks as a status change.

  • refunded with refund_confirmed means the refund executed.
  • failed with expired or origin_transfer_failed means the funds never moved.
  • A transfer whose deposit was made but which has not yet completed or been refunded stays pending. It does not become expired.
  • If a transfer is still pending well past originValidUntil plus expectedRefundTimeMs, contact Across with the orderId.

Once refunded, transfer.refund carries output.token. chain, recipient and output.minAmount can be null, so handle each being missing. minAmount is the floor the quote guaranteed, not the amount received. The refund transaction is the transactions[] entry with type: "refund"; read it when transfer.refund is absent.

isTerminal can flip back until finalized is true. Wait for finalized before acting on the outcome, for example before crediting the user again.

wait-for-outcome.ts
const BASE_URL = "https://api.across.to";
const API_KEY = process.env.ACROSS_API_KEY;

async function waitForOutcome(orderId: string) {
  while (true) {
    const res = await fetch(`${BASE_URL}/v1/transfers/${orderId}`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    const transfer = await res.json();
    const { state, reason, isTerminal, finalized } = transfer.status;

    // state is now "completed", "refunded" or "failed"
    if (isTerminal && finalized) return transfer;

    console.log(`${state} / ${reason}`);
    await new Promise((r) => setTimeout(r, 60_000)); // refunds can take a while
  }
}

On this page