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 at most four HTTP calls: the submit call is needed only when the quote asks for it.
Staging environment. These endpoints run against staging.
Before you start
You need an API key. Every Across API endpoint rejects unauthenticated
requests with 401.
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 | Return only tokens available against the opposite selection |
capabilities.operations | Declare what your client can do (approve_token:evm, sign_typed_data:evm, send_transaction:evm, submit_execution, …) so tokens you cannot fund are marked unavailable |
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 actions to execute it.
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",
}),
});
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 don't name a funding method. The server picks one of approval,
permit2 or erc3009 from what your client can do — declare that with
capabilities.operations, and narrow it with authorizationPolicy.allowedTypes
— and returns the matching nextActions. Sending the old fundingMethods
field is rejected with 400 request.unsupported_param.
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 |
|---|---|
approve_token | Send the ERC-20 approval transaction |
sign_typed_data | Feed typedData to eth_signTypedData_v4 and keep the signature |
send_transaction | Send the supplied transaction (ecosystem says whether it is EVM or Solana) |
sign_transaction | Sponsored Solana only: co-sign the supplied transaction without broadcasting it |
submit_execution | POST the results of the earlier actions to the executions endpoint. Present only when that POST is needed |
For a permit2 or erc3009 quote you get a sign_typed_data 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_typed_data");
const signatures = {
[signAction.id]: await walletClient.signTypedData(signAction.typedData),
};
const execRes = await fetch(`${BASE}/v1/quotes/${quote.orderId}/executions`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ authorization: { signatures } }),
});
const execution = await execRes.json();The response carries orderId, acceptedAt and, when a fresh balance reading
was taken, balanceCheck.
For an approval quote, the actions are an approve_token (omitted when the
allowance already covers the amount) and a send_transaction with
id: "prefunded-store", the deposit itself. There is no submit_execution:
send the approval, then the deposit, and the transfer starts once the deposit
lands on chain. Don't POST to the executions endpoint for it. The
Quickstart has both flows as code.
The transfer you track is addressed by its order id — the same orderId
the quote returned and the execution response echoes. Pass it to
GET /v1/transfers/{orderId}.
A 2xx here means the transfer was accepted, not that it has arrived.
Track it to a settled outcome
Track the transfer with the same key:
const transferRes = await fetch(`${BASE}/v1/transfers/${execution.orderId}`, {
headers: auth,
});
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 | finding_relayer, awaiting_execution |
completed | filled, filled_destination_action_successful, filled_destination_action_failed |
refunded | refund_confirmed |
failed | expired, origin_transfer_failed, submission_rejected |
A transfer with destinationActions completes as
filled_destination_action_successful or filled_destination_action_failed,
never plain filled.
isTerminal is the stopping signal — poll about every 5 seconds and stop
when it is true. finalized reports that the chain can no longer revise the
outcome; a normal integration does not need to wait for it.
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 arrive in two envelopes. Requests rejected at the edge are flat —
{ "code": "unauthenticated", "message": "AUTH_INVALID", "details": null }.
Everything that reaches a service nests under error, with a hierarchical
type.specific code and a requestId:
{
"error": {
"type": "request",
"code": "request.unsupported_param",
"message": "unsupported field: amountTyp",
"param": "amountTyp",
"requestId": "bhmecsejc58sfsfe2u56mn4cfc"
}
}Read error.code first and fall back to the top-level code.
| Situation | What you get |
|---|---|
| Missing or invalid API key | 401 — flat unauthenticated, or error.code of auth.invalid_key |
| Unrecognised body or query field | 400 request.unsupported_param, naming the field |
| Amount the route cannot clear | 400 policy.amount_out_of_range |
| Unknown or malformed order id | 404 not_found.transfer |
| 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.