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 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.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
reachableOnlyDrop anything that cannot currently be routed
capabilities.fundingMethodsFilter to routes supporting approval, permit2, erc3009 or deposit_address
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 payload to sign.

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",
    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:

kindWhat to do
approvalSend the ERC-20 approval transaction
signFeed typedData to eth_signTypedData_v4 and keep the signature
transactionSend the supplied transaction
deposit_addressSend 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:

statereason
pendingawaiting_auction, funds_received_awaiting_auction, awaiting_origin_execution, awaiting_destination_execution, awaiting_delivery
completedfilled
refundedrefund_confirmed
failedexpired, 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.

SituationWhat you get
Missing or bad API key401 unauthenticated
Unknown route404 not_found
Misspelled body fieldValidation error — bodies are strict
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