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.
refundedwithrefund_confirmedmeans the refund executed.failedwithexpiredororigin_transfer_failedmeans the funds never moved.- A transfer whose deposit was made but which has not yet completed or been
refunded stays
pending. It does not becomeexpired. - If a transfer is still
pendingwell pastoriginValidUntilplusexpectedRefundTimeMs, contact Across with theorderId.
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.
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
}
}