Getting Started [#getting-started]
We recommend using the **Swap API** for most integrations — it's the most flexible and well-documented path.
* **[Swap API](/introduction/swap-api)** — Full control over the bridging flow. Ideal for wallets, dApps, aggregators, and backend services.
* **[App SDK](/guides/dev-guides/integrate-swap-api)** — TypeScript SDK that wraps the API. Good for quick prototyping.
Start with the [step-by-step integration guide](/guides/dev-guides/integrate-swap-api) to get up and running.
**Yes.** API keys should never be exposed in client-side code — they can be extracted from browser network requests and abused. Move your Across API calls to a server-side route (e.g. a Next.js API route, Express endpoint, or serverless function) and pass the API key there via the `Authorization` header.
Beyond security, API keys and Integrator IDs are important because they help us:
* **Track your usage** — so we can provide you with analytics and support
* **Monitor for anomalies** — if your integration gets attacked or experiences unusual traffic, we can detect it early and alert you
* **Prioritize your requests** — authenticated requests receive higher rate limits suitable for production traffic
[Get your API key and Integrator ID](/tools/integrator-id) if you haven't already.
**No.** Testnet endpoints do not require authentication. You can develop and test your integration freely without credentials. Only production requests need an API key.
See [API Keys & Integrator ID](/introduction/api-keys) for details on environments and rate limits.
When using the legacy `/suggested-fees` API with direct `depositV3` SpokePool calls, credentials are passed differently than the Swap API:
* **API key** — pass as a `Bearer` token in the `Authorization` header on the `/suggested-fees` request itself
* **Integrator ID** — append to the `depositV3` transaction **calldata** (not as a query parameter)
To append the Integrator ID, add a **delimiter** (`1dc0de`) followed by your **ID** to the end of the encoded calldata:
```
...0000000000000000 // last depositV3 param
1dc0def001 // ← delimiter (1dc0de) + integrator ID (f001)
```
```ts
import { encodeFunctionData, type Hex } from "viem";
const DELIMITER = "1dc0de";
const INTEGRATOR_ID = "f001"; // Replace with your assigned ID
const calldata = encodeFunctionData({
abi: spokePoolAbi,
functionName: "depositV3",
args: [depositor, recipient, inputToken, outputToken, inputAmount,
outputAmount, destinationChainId, exclusiveRelayer,
quoteTimestamp, fillDeadline, exclusivityDeadline, message],
});
const calldataWithId = `${calldata}${DELIMITER}${INTEGRATOR_ID}` as Hex;
```
**Important:** Do NOT pass the delimiter + ID to any `depositV3` parameter (including `message`). Only append it to the raw calldata. Do NOT use `f001` — it's an example. [Get your own ID](/tools/integrator-id).
Full details at [API Keys & Integrator ID — Legacy Suggested Fees](/introduction/api-keys).
> **Warning:** The `/suggested-fees` API is legacy and will be sunset. New integrations should use the [Swap API (`/swap/approval`)](/api-reference/swap/approval/get) instead, which handles the Integrator ID as a simple query parameter — no calldata appending needed. See [Migrate from Suggested Fees to the Swap API](/guides/migration/suggested-fees-to-swap) for a step-by-step guide.
Token Behavior [#token-behavior]
Depending on the context, Across will sometimes send a user ETH and sometimes send a user WETH:
| Input Asset | Recipient Type | Received |
| ----------- | -------------- | -------- |
| **ETH** | EOA (wallet) | **ETH** |
| **ETH** | Contract | **WETH** |
| **WETH** | EOA (wallet) | **ETH** |
| **WETH** | Contract | **WETH** |
This behavior is automatic — you don't need to handle wrapping/unwrapping yourself. See the [Swap API docs](/introduction/swap-api) for more details.
To deposit **ETH** (or the native token for a network), set `msg.value = inputAmount`. To deposit **WETH**, set `msg.value = 0`.
In both cases, the `inputAmount` should be the same and the `inputToken` should be the **WETH address** for the current chain.
```ts
// ETH deposit
await contract.deposit({ ..., value: inputAmount });
// WETH deposit
await contract.deposit({ ..., value: 0n });
```
Find WETH addresses per chain via [/swap/tokens](/api-reference/swap/tokens/get).
Deposits & Refunds [#deposits--refunds]
Don't panic — your funds are safe. Here's what to do:
1. **Check the status** using the [Status Tracker](/tools/status-tracker) tool or the [deposit status API](/api-reference/deposit/status/get)
2. **If `pending`** — the transfer is still being processed. Most deposits fill in \~2 seconds, but it can take longer during congestion
3. **If `expired`** — a refund is being processed automatically. See [Refunds](/introduction/refunds) for timing (\~120-150 minutes)
4. **If stuck for >3 hours** — reach out via [Telegram](https://t.me/acrosstg), [in-app support](https://across.to), or Slack
Note: refunds are only issued on mainnet deposits.
Recommended `fillDeadline` is **6 hours** from the time of deposit. This ensures the best experience for the user, especially in tricky situations where a deposit is being [slow filled](/guides/concepts/intent-lifecycle).
A shorter `fillDeadline` risks the deposit expiring before it can be filled during periods of high congestion or relayer downtime. See [Refunds](/introduction/refunds) for what happens when a deposit expires.
Refund timing after a deposit expires:
| Metric | Time after deposit |
| ----------- | ------------------ |
| **Minimum** | \~120 minutes |
| **Typical** | \~135 minutes |
| **Maximum** | \~150 minutes |
Components: fill deadline (\~90 min) + bundle processing (\~30 min) + canonical bridge delay (\~5-30 min).
If your refund hasn't arrived after **3 hours**, reach out to the team. Full details at [Refunds](/introduction/refunds).
Deposit Addresses [#deposit-addresses]
It still works. Deposit addresses are **amount agnostic** — the address is tied to the route (origin/destination chain, tokens, recipient, refund address), not to a specific amount. The sweeper bot re-quotes at sweep time using the actual balance that arrived at the address, so users can send more or less than originally quoted and the bridge will still execute. The output will reflect the amount actually sent and the fee bounds locked in at quote time.
Amount agnostic does not mean unbounded: for [`POST /deposit-addresses`](/api-reference/deposit-addresses/post), the amount must still fall within the `limits` (`minInputAmount` / `maxInputAmount`) of the `supportedInputs[]` entry matching the origin chain and token you send. Amounts outside those bounds are refunded rather than swept.
See [Persistent Deposit Addresses](/introduction/persistent-deposit-addresses) for more details.
No. Addresses returned by [`POST /deposit-addresses`](/api-reference/deposit-addresses/post) are **persistent and never expire** — `expiresAt` is always `null`. The same request parameters always resolve to the same address, so you can request it once and reuse it indefinitely.
What bounds a transfer is the route, not the clock. A deposit is swept automatically when its origin chain and token match an entry in the response's `supportedInputs[]` **and** the amount falls inside that entry's `limits` (`minInputAmount` / `maxInputAmount`). A deposit outside those bounds can't be completed and takes the refund path — automatic to your `refundAddresses` if you supplied them, manual recovery via support if you didn't. `supportedInputs[]` changes as origin chains and limits are added, so re-POST before a reuse that depends on limits you read earlier: you get the same address back, with a current list.
If a transfer that satisfied those bounds hasn't been swept, it is **not** because the address expired. Reach out to the team with your `depositAddress` and the transfer transaction hash via [Telegram](https://t.me/acrosstg), [in-app support](https://across.to), or Slack.
Addresses issued by the deprecated `GET /swap/counterfactual` endpoint did carry a 24-hour TTL, and funds sent after expiry were not processed automatically. That limit never applied to `POST /deposit-addresses`.
Migrating off the legacy endpoint depends on your destination. [`POST /deposit-addresses`](/api-reference/deposit-addresses/post) now covers 20 destination chains — see [Supported routes](/introduction/persistent-deposit-addresses#supported-routes) — so most integrations can move across.
See [Persistent Deposit Addresses](/introduction/persistent-deposit-addresses) for the full flow.
All scenarios require manual recovery — reach out to the team with your `depositAddress`, the token address you sent, and the chain ID where funds were sent.
| Scenario | Outcome |
| -------------------------- | ------------------------ |
| Correct token, wrong chain | Manual recovery required |
| Wrong token, correct chain | Manual recovery required |
| Wrong token, wrong chain | Manual recovery required |
Full details at [Persistent Deposit Addresses](/introduction/persistent-deposit-addresses).
***