End-to-end V5 integration
Discover, price, execute and track a transfer against the V5 staging environment.
This guide walks one transfer all the way through Across V5: find out what can move, price it, get it signed, submit it, and follow it to a settled outcome. It is four HTTP calls.
Staging environment. These endpoints run against staging.
Before you start
You need an API key. GET /v1/capabilities, POST /v1/quotes and
POST /v1/quotes/{orderId}/executions all reject unauthenticated requests
with 401 unauthenticated.
Every V5 endpoint is served from one host, under /v1:
https://api.staging.across.toCheck you can reach the Integrator API — this route needs no key:
curl "https://api.staging.across.to/v1/quotes/ping"
# {"ok":true,"service":"quote"}const BASE = "https://api.staging.across.to";
const auth = { Authorization: `Bearer ${process.env.ACROSS_API_KEY}` };Discover what can move
GET /v1/capabilities answers "what can I move between these two chains right
now" — and, importantly, tells you why anything unavailable is unavailable
rather than silently omitting it.
const params = new URLSearchParams({
originChainId: "42161",
destinationChainId: "8453",
include: "tokens",
});
const res = await fetch(`${BASE}/v1/capabilities?${params}`, { headers: auth });
const capabilities = await res.json();Useful query parameters:
| Parameter | Purpose |
|---|---|
include | chains, tokens, or both |
originChainId / destinationChainId | Narrow to one corridor |
originToken / destinationToken | Narrow to one asset |
reachableOnly | Drop anything that cannot currently be routed |
capabilities.fundingMethods | Filter to routes supporting approval, permit2, erc3009 or deposit_address |
limit, originCursor, destinationCursor | Keyset pagination |
Availability here means selectable, not guaranteed to price. POST /v1/quotes
is the binding check for one specific transfer.
Take the decimals for the token you selected from this response. Amounts
everywhere else are integers in base units, sent as strings — and decimals for
the same asset differ per chain, so never hardcode them.
Price the transfer
POST /v1/quotes prices one exact transfer and returns the payload to sign.
Which side carries the amount depends on amountType:
amountType | Meaning | Amount goes on |
|---|---|---|
exact_input (default) | Spend a known input amount | origin.amount |
exact_output | Deliver an exact output amount | destination.amount |
min_output | Deliver at least a floor amount | destination.amount |
exact_input is the only mode that may leave amountType off. The other two
must name it explicitly.
const quoteRes = await fetch(`${BASE}/v1/quotes`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
origin: {
token: { chainId: 42161, address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" },
amount: "1000000", // 1 USDC, 6 decimals
depositor: "0x1111111111111111111111111111111111111111",
},
destination: {
token: { chainId: 8453, address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" },
recipient: "0x1111111111111111111111111111111111111111",
},
amountType: "exact_input",
fundingMethods: ["permit2"],
}),
});
const quote = await quoteRes.json();Request bodies are strict. Unknown fields are rejected rather than ignored, so a misspelled key is an error. Quotes expire roughly 15 minutes after issue.
You can request approval, permit2 or erc3009. The remaining funding
methods — deposit_address, account_abstraction, contract — are chosen by
the server and returned on the quote rather than requested.
Execute the quote
The quote comes back with nextActions: an ordered list of things that have to
happen before the transfer can proceed. Dispatch on kind:
kind | What to do |
|---|---|
approval | Send the ERC-20 approval transaction |
sign | Feed typedData to eth_signTypedData_v4 and keep the signature |
transaction | Send the supplied transaction |
deposit_address | Send funds to the address in quote.depositInstructions |
For a permit2 or erc3009 quote you get a sign action. Sign its
typedData verbatim — that exact payload is what the server verifies — then
submit the signature:
const signAction = quote.nextActions.find((a) => a.kind === "sign");
const signatures = {
[signAction.id]: await walletClient.signTypedData(signAction.typedData),
};
const execRes = await fetch(`${BASE}/v1/quotes/${quote.quoteId}/executions`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ authorization: { signatures } }),
});
const execution = await execRes.json();The response carries executionId, transferId, quoteId and acceptedAt.
quoteId, transferId and the Across order id are the same value — the
entry-step merkle root of the order. Nothing needs translating between the
quote you priced and the transfer you track.
A 2xx here means the transfer was accepted, not that it has arrived.
Track it to a settled outcome
Transfer status needs no API key:
const transferRes = await fetch(`${BASE}/v1/transfers/${execution.transferId}`);
const transfer = await transferRes.json();
console.log(transfer.status);
// { state, reason, description, isTerminal, finalized }GET /v1/transfers lists transfers newest-first with keyset pagination when
you need more than one.
Reading status
status.state is the coarse outcome and status.reason is the granular one.
Each reason maps to exactly one state:
state | reason |
|---|---|
pending | awaiting_auction, funds_received_awaiting_auction, awaiting_origin_execution, awaiting_destination_execution, awaiting_delivery |
completed | filled |
refunded | refund_confirmed |
failed | expired, source_transfer_failed |
Two fields decide when to stop polling:
isTerminal— Across will take no further action on this transfer.finalized— the chain state behind it can no longer reorg.
Poll until isTerminal, then slow down rather than stopping outright. A
destination reorg after a terminal-but-not-finalized read is rare, but the
indexer corrects itself and a UI that stopped polling will keep showing the
stale outcome.
status.description is a support-safe sentence you can surface directly to a
user. Branch your own logic on state and reason, never on description.
Handling errors
Errors carry a machine-readable code and a human message. Branch on code.
| Situation | What you get |
|---|---|
| Missing or bad API key | 401 unauthenticated |
| Unknown route | 404 not_found |
| Misspelled body field | Validation error — bodies are strict |
| Quote past its expiry | Re-quote; do not retry the execution |
What is not here
The pre-V5 endpoints — the Swap API, Suggested Fees, gasless, persistent deposit addresses, webhooks — are collected under Deprecated APIs in the API reference. They still work, and existing integrations are unaffected. New integrations should use the four calls above.