Across Docs
Developer Guides

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.to

Check 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:

ParameterPurpose
includechains, tokens, or both
originChainId / destinationChainIdNarrow to one corridor
originToken / destinationTokenNarrow to one asset
reachableOnlyReturn only tokens available against the opposite selection
capabilities.operationsDeclare 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, destinationCursorKeyset 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:

amountTypeMeaningAmount goes on
exact_input (default)Spend a known input amountorigin.amount
exact_outputDeliver an exact output amountdestination.amount
min_outputDeliver at least a floor amountdestination.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:

kindWhat to do
approve_tokenSend the ERC-20 approval transaction
sign_typed_dataFeed typedData to eth_signTypedData_v4 and keep the signature
send_transactionSend the supplied transaction (ecosystem says whether it is EVM or Solana)
sign_transactionSponsored Solana only: co-sign the supplied transaction without broadcasting it
submit_executionPOST 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:

statereason
pendingfinding_relayer, awaiting_execution
completedfilled, filled_destination_action_successful, filled_destination_action_failed
refundedrefund_confirmed
failedexpired, 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.

SituationWhat you get
Missing or invalid API key401 — flat unauthenticated, or error.code of auth.invalid_key
Unrecognised body or query field400 request.unsupported_param, naming the field
Amount the route cannot clear400 policy.amount_out_of_range
Unknown or malformed order id404 not_found.transfer
Quote past its expiryRe-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.

On this page