# What is Swapper?

Swapper is a **deposit layer you embed in your app**. Drop in one widget and your users can fund your product with **any token, from any chain, or with a debit card** — and you receive exactly the token you asked for, on the chain you asked for, at the wallet you nominated.

Here is the core idea:

1. You register as an **integrator** and decide on a **destination**: a chain, a token, and a wallet address that should receive funds.
2. You embed the **Swapper widget** in your app — as an iframe, a modal, or an inline component.
3. Your user picks how they want to pay: **card**, **crypto transfer from their wallet**, or a **one-tap smart-wallet deposit**.
4. Swapper handles the on-ramp, the cross-chain routing, and the swap, then delivers the destination token to your wallet.
5. You find out it happened — instantly in the browser via **widget events**, or server-side via **webhooks**.

## Why Swapper?

* **One integration, every funding path.** Fiat card payments, same-chain and cross-chain crypto transfers, and account-abstraction deposits all live behind a single widget with four required parameters.
* **You choose what you receive.** The user's messy reality — a dozen tokens across a dozen chains — is normalized to the one token and chain your app actually wants. See [Core Concepts](/getting-started/core-concepts).
* **Drop-in, framework-agnostic.** You embed Swapper through the hosted widget via the [iframe SDK](/widget-integration/iframe-sdk) — it works in any stack, in any framework or none.
* **Yours to brand.** Theme the widget with your colors, radius, and copy so it feels native to your product. See [Styling & Theming](/widget-integration/styling).
* **Programmable.** Chain arbitrary on-chain actions after the swap — stake, wrap, provide liquidity — with [Custom Contract Calls](/widget-integration/custom-contract-calls).

## Who is Swapper for?

* **Integrators & developers** — you run an app (a game, an exchange, a prediction market, a DeFi protocol, a wallet) and you want users to fund it without wrestling with bridges, DEXes, and on-ramps. Start with the [Quick Start](/getting-started/quick-start).
* **Product & growth teams** — you want the highest-converting funding flow you can ship in an afternoon and measure end to end. Skim [How Swapper Works](/getting-started/how-swapper-works) and [Tracking Deposits](/tracking-deposits/widget-events).

{% hint style="info" %}
**Getting an `integratorId`.** Every integration is tied to a registered integrator id, which also links your [webhook endpoint](/tracking-deposits/webhooks). To get one, open a ticket in [Discord](https://discord.gg/y8eevERxBz) — see [Support](/resources/support).
{% endhint %}

## Where to go next

| You want to…                         | Start here                                                                                  |
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
| Understand the model in five minutes | [How Swapper Works](/getting-started/how-swapper-works)                                     |
| Ship an embed today                  | [Quick Start](/getting-started/quick-start)                                                 |
| Pick the right integration style     | [Choosing an Integration](/widget-integration/choosing-an-integration)                      |
| Get notified when deposits complete  | [Widget Events](/tracking-deposits/widget-events) & [Webhooks](/tracking-deposits/webhooks) |


# How Swapper Works

Swapper sits between **however your user has money today** and **the exact token your app wants to receive**. This page explains that flow end to end — nothing here is required to integrate, but it makes the rest of the docs click.

## The one-sentence version

You declare a **destination** (chain + token + wallet); the user picks a **funding method**; Swapper does the on-ramp / bridge / swap and delivers your token; you get **notified** when it lands.

## The pieces

Four moving parts sit between your user and your wallet:

* **Your app** — embeds the widget with the four required parameters (`integratorId`, `dstChainId`, `dstTokenAddr`, `depositWalletAddress`).
* **The funding method** the user picks — card on-ramp, crypto transfer, or wallet deposit.
* **The Swapper backend** — routing, pricing, and deposit tracking; it swaps and bridges the funds to your destination token.
* **Your destination wallet** — receives the token, and the outcome is reported back to your app by [widget event](/tracking-deposits/widget-events) and [webhook](/tracking-deposits/webhooks).

## The four required parameters

Every integration is configured with exactly four things:

| Parameter              | What it is                                                                             |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `integratorId`         | Your registered identity. Ties the session to your config, fees, and webhook endpoint. |
| `dstChainId`           | The chain you want to receive funds on (e.g. `"8453"` for Base).                       |
| `dstTokenAddr`         | The token you want to receive on that chain (`0x0…0` for native).                      |
| `depositWalletAddress` | The wallet that should receive the destination token.                                  |

Everything else — styling, deposit-method selection, limits, custom calls — is optional. See the [Configuration Reference](/widget-integration/configuration).

## The three funding methods

The user chooses on the widget's home screen; you can restrict which appear with [`supportedDepositOptions`](/widget-integration/configuration#supporteddepositoptions).

* **Pay with card** (`depositWithCash`) — a fiat on-ramp (card / Apple Pay / bank) turns cash into crypto, which is then swapped to your destination token. See [Pay with Card](/deposit-methods/fiat-onramp).
* **Transfer crypto** (`transferCrypto`) — a smart-wallet / account-abstraction flow that lets a user deposit and swap in a single tap, and can even sweep an on-ramp onward automatically. See [Transfer Crypto](/deposit-methods/transfer-crypto).
* **Wallet deposit** (`walletDeposit`) — the user sends tokens from their own wallet on any supported chain; Swapper bridges and swaps to your destination. See [Wallet Deposit](/deposit-methods/wallet-deposit).

## Asynchronous by nature

Cross-chain routing, on-ramp settlement, and swap confirmation don't all happen in the same instant. The widget guides the user through each step and shows progress; your app learns the final outcome through two independent channels:

* [**Widget events**](/tracking-deposits/widget-events) — a `postMessage` fired to the parent page the moment a deposit succeeds. Great for updating the UI.
* [**Webhooks**](/tracking-deposits/webhooks) — a signed server-to-server callback when a deposit completes. Use this as your source of truth for crediting accounts; browsers can be closed, servers don't miss the event.

{% hint style="info" %}
Use **both**: the widget event for instant UX, the webhook for reliable accounting. They describe the same deposit from two vantage points.
{% endhint %}

## What Swapper does *not* do

* It never takes custody of your users' funds between steps beyond what the swap itself requires — routing is executed from the user's wallet or a per-user smart wallet.
* It doesn't hold your destination funds — they go straight to `depositWalletAddress`.
* It doesn't require an API key to render; your `integratorId` is what scopes a session.


# Quick Start

Embed a working deposit widget in about five minutes.

## 1. Get an `integratorId`

Every integration is tied to a registered integrator id. It scopes the widget session to your configuration and links your [webhook](/tracking-deposits/webhooks). To get one, open a ticket in [Discord](https://discord.gg/y8eevERxBz) — see [Support](/resources/support).

You can build against the examples below with a placeholder id, but a real deposit needs a registered one.

## 2. Decide your destination

Pick the three values that describe **what you want to receive and where**:

* `dstChainId` — e.g. `"8453"` (Base)
* `dstTokenAddr` — e.g. `"0x833…913"` (USDC on Base)
* `depositWalletAddress` — the wallet that should receive it

The zero address (`0x0000000000000000000000000000000000000000`) means the chain's native token.

## 3. Install the SDK

Swapper embeds through the **iframe SDK** ([`@swapper-finance/deposit-sdk`](https://www.npmjs.com/package/@swapper-finance/deposit-sdk)) — works in any framework, or none:

```bash
npm install @swapper-finance/deposit-sdk
```

## 4. Embed the widget

### As a modal (fastest to ship)

```typescript
import { openSwapperModal } from "@swapper-finance/deposit-sdk";

document.querySelector("#fund").addEventListener("click", () => {
  openSwapperModal({
    integratorId: "your-integrator-id",
    dstChainId: "8453",
    dstTokenAddr: "0x833…913",
    depositWalletAddress: "0x2A0…28A",
    onEvent: (event) => console.log("widget event:", event),
  });
});
```

### Inline, in a container

```typescript
import { SwapperIframe } from "@swapper-finance/deposit-sdk";

const swapper = new SwapperIframe({
  container: "#swapper-container", // CSS selector or HTMLElement
  integratorId: "your-integrator-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
});
```

That's it — the widget loads [`https://deposit.swapper.finance`](https://deposit.swapper.finance) in a sandboxed iframe and renders your branded deposit flow.

## 5. React to a completed deposit

Pass an `onEvent` callback to react in the browser the moment a deposit lands:

```typescript
import { WidgetEventName } from "@swapper-finance/deposit-sdk";

openSwapperModal({
  integratorId: "your-integrator-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
  onEvent: (event) => {
    if (event.type === WidgetEventName.TRANSACTION_SUCCESS) {
      console.log("Deposit complete:", event.data);
      // update your UI here
    }
  },
});
```

For reliable, server-side crediting, also set up a [webhook](/tracking-deposits/webhooks) — it's the source of truth even if the user closes the tab.

## 6. Make it yours

Add a `styles` object to match your brand:

```typescript
openSwapperModal({
  // ...required params
  styles: {
    themeMode: "dark",
    componentStyles: {
      primaryColor: "#836FFF",
      backgroundColor: "#111111",
    },
  },
});
```

See [Styling & Theming](/widget-integration/styling) for the full list.

## Next steps

* **Which embed style fits you?** → [Choosing an Integration](/widget-integration/choosing-an-integration)
* **Every option explained** → [Configuration Reference](/widget-integration/configuration)
* **Track deposits server-side** → [Webhooks](/tracking-deposits/webhooks)


# Core Concepts

A short tour of the ideas the rest of the docs assume. If you've read [How Swapper Works](/getting-started/how-swapper-works), this fills in the vocabulary.

## Integrator

You. An **integrator** is any app that embeds the Swapper widget. Each integrator has a registered **`integratorId`** that ties a session to your configuration, your fee arrangement, and your webhook endpoint. Contact Swapper to get one (see [Support](/resources/support)).

## Destination

The **destination** is the outcome you want, described by three values:

* **`dstChainId`** — the chain funds arrive on.
* **`dstTokenAddr`** — the token you receive there (`0x0…0` = native).
* **`depositWalletAddress`** — the wallet that receives it.

No matter what the user pays with, Swapper normalizes it to this destination.

## Source

What the user actually pays with — a card charge, USDC on Arbitrum, ETH on Ethereum, SOL on Solana, etc. Sources span **multiple ecosystems** (EVM, Solana, and more). The set of sources that can reach a given destination is determined by Swapper's routing engine.

## Deposit methods

The three ways a user can fund, chosen on the widget's home screen:

| Method          | Key               | In short                                   |
| --------------- | ----------------- | ------------------------------------------ |
| Pay with card   | `depositWithCash` | Fiat on-ramp → crypto → your token         |
| Transfer crypto | `transferCrypto`  | One-tap smart-wallet deposit & swap        |
| Wallet deposit  | `walletDeposit`   | Send from the user's own wallet, any chain |

You can show all of them or restrict the set with [`supportedDepositOptions`](/widget-integration/configuration#supporteddepositoptions). See [Deposit Methods](/deposit-methods/overview) for the details of each.

## Route

For crypto funding, Swapper computes a **route**: the cheapest/best path to swap and (if needed) bridge the source into the destination token. A route quote includes the estimated and minimum output, the fees, and the exact transactions to sign.

## The deposit record

Every completed funding produces a **deposit** — a unified record that captures the source asset, the destination asset, the tx hashes involved, and a lifecycle `status`. It's what you receive in a [webhook](/tracking-deposits/webhooks). Its `method` is one of `onramp`, `smart_wallet`, or `dex_swap`.

## Two notification channels

Swapper tells your app a deposit happened in two independent ways:

* [**Widget events**](/tracking-deposits/widget-events) — a `postMessage` to the parent page, in the browser, the instant it succeeds.
* [**Webhooks**](/tracking-deposits/webhooks) — a signed HTTP callback to your server when the deposit completes.

Use the event for instant UX and the webhook as your reliable source of truth.

## Amounts are base-unit strings

Everywhere Swapper reports a token amount — in webhooks, in events — it is the **integer amount in the token's smallest unit, serialized as a string**: wei for 18-decimal tokens, `1_000_000` for 1 USDC (6 decimals), lamports for Solana. Never a float. Parse with a big-integer library. Prices and USD values are decimal strings.

## The widget is hosted; you embed it

The deposit UI is a hosted app at [`https://deposit.swapper.finance`](https://deposit.swapper.finance). Your integration loads it in a sandboxed iframe via the [iframe SDK](/widget-integration/iframe-sdk), configured with the same four required parameters. See [Choosing an Integration](/widget-integration/choosing-an-integration).


# Glossary

Quick reference for the terms you'll meet across these docs and the SDK.

### Integrator

Any app embedding the Swapper widget. Identified by an `integratorId`.

### `integratorId`

Your registered integrator identity. Scopes a widget session to your config and links your webhook endpoint.

### Destination

The `dstChainId` + `dstTokenAddr` + `depositWalletAddress` triple describing what you want to receive and where. See [Core Concepts](/getting-started/core-concepts#destination).

### Source

Whatever the user funds with — a card payment or a token on some chain — before Swapper converts it to your destination token.

### Deposit method / deposit option

One of the funding flows: `depositWithCash` (card), `transferCrypto`, or `walletDeposit`. Controlled with `supportedDepositOptions`.

### Route

The computed swap/bridge path from a source to the destination — the widget quotes it, along with the transactions to sign and the fee breakdown.

### Deposit (record)

The unified record of a completed funding: source & destination assets, tx hashes, and lifecycle `status`. Its `method` is `onramp`, `smart_wallet`, or `dex_swap`.

### On-ramp

A fiat-to-crypto payment (card, Apple Pay, bank transfer) provided through Swapper's on-ramp partner. Backs the `depositWithCash` method.

### Smart wallet

A per-user account-abstraction wallet (deployed via ZeroDev / EIP-7702) that lets a user deposit and swap in a single tap, and can sweep on-ramp funds onward. Backs the `transferCrypto` option (deposit `method` `smart_wallet`).

### Custom contract call

An extra on-chain action executed after the swap — approve, stake, wrap, provide liquidity — configured with `customContractCalls`. See [Custom Contract Calls](/widget-integration/custom-contract-calls).

### `ContractCallType`

How a custom call treats amounts: `DEFAULT` (0), `FULL_TOKEN_BALANCE` (1), `FULL_NATIVE_BALANCE` (2), `COLLECT_TOKEN_BALANCE` (3).

### Widget event

A `postMessage` the iframe fires to the parent page (envelope `type: "SWAPPER_EVENT"`). Names: `transaction_success`, `resize`, `close_request`. See [Widget Events](/tracking-deposits/widget-events).

### Webhook

A signed server-to-server HTTP callback fired when a deposit completes (`eventType: transaction.completed`). See [Webhooks](/tracking-deposits/webhooks).

### `X-Webhook-Signature`

The HMAC-SHA256 (Base64) signature header on every webhook request, computed over the raw body with your integrator secret. Verify it before trusting a payload.

### Base unit

The smallest integer unit of a token (wei, lamports, `1e6` for USDC). All amounts in events and webhooks are base-unit **strings**.

### Ecosystem

A family of chains that share an address/transaction format: `evm`, `solana`, `starknet`, `fast`.

### Iframe SDK — `@swapper-finance/deposit-sdk`

The package that embeds the hosted widget as an iframe, modal, or inline embed. The only package you install to integrate Swapper.

### `actionLabel`

UI wording mode: `"buy"` (default — "Buy with Fiat") or `"deposit"` ("Deposit with Fiat").

### `depositWalletAddress`

The wallet that receives the destination token. Not a Swapper-custodied account — it's yours.


# Choosing an Integration

There is one deposit widget and a few ways to put it on your page. Pick by your stack and how much control you want over presentation.

## At a glance

| Approach                 | Package                                                          | Best for                                                                        |
| ------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| **Iframe SDK**           | [`@swapper-finance/deposit-sdk`](/widget-integration/iframe-sdk) | Almost everyone. Any framework or no framework.                                 |
| **Modal / Inline embed** | `@swapper-finance/deposit-sdk`                                   | A focused popup, or a placeholder that fills in-place. Built on the iframe SDK. |

## The recommended path: the iframe SDK

For the vast majority of integrations, use [`@swapper-finance/deposit-sdk`](/widget-integration/iframe-sdk). It loads the hosted widget ([`https://deposit.swapper.finance`](https://deposit.swapper.finance)) inside a sandboxed iframe and gives you a small, typed API to configure it, listen for events, and update it on the fly.

Why the iframe SDK:

* **Framework-agnostic** — vanilla JS, React, Vue, Svelte, Angular, mobile WebViews.
* **Always current** — the widget is hosted, so wallet SDKs, chains, and on-ramp logic update without you shipping anything.
* **Isolated** — the widget's Web3 stack runs in the iframe, sandboxed from your app.

It comes in three shapes, all in the same package:

* [**`SwapperIframe`**](/widget-integration/iframe-sdk) — mount inline in a container you control.
* [**`SwapperModal` / `openSwapperModal`**](/widget-integration/modal-and-embed) — a centered popup with a backdrop.
* [**`SwapperEmbed`**](/widget-integration/modal-and-embed#inline-embed-swapperembed) — preload hidden, then reveal over a placeholder for an instant open.

## Decision guide

* **Want the fastest path to a working deposit?** → [Quick Start](/getting-started/quick-start) with `openSwapperModal`.
* **Embedding in a page layout?** → [`SwapperIframe`](/widget-integration/iframe-sdk) in a container, or [`SwapperEmbed`](/widget-integration/modal-and-embed#inline-embed-swapperembed) if you want instant open.


# Iframe SDK

[`@swapper-finance/deposit-sdk`](https://www.npmjs.com/package/@swapper-finance/deposit-sdk) is the recommended way to integrate Swapper. It embeds the hosted widget ([`https://deposit.swapper.finance`](https://deposit.swapper.finance)) in a sandboxed iframe and exposes a small, typed API to configure it, listen for events, and update it live.

* Written in TypeScript, typed API.
* Works in any framework or none, including mobile WebViews.
* `postMessage` communication with origin validation.

```bash
npm install @swapper-finance/deposit-sdk
```

## `SwapperIframe`

The core class. It builds an iframe, validates your config, and mounts it into a container.

```typescript
import { SwapperIframe } from "@swapper-finance/deposit-sdk";

const swapper = new SwapperIframe({
  container: "#swapper-container", // CSS selector or HTMLElement
  integratorId: "your-integrator-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
});
```

If you omit `container`, mount later:

```typescript
const swapper = new SwapperIframe({ /* config, no container */ });
swapper.mount("#swapper-container");
```

The four required parameters (`integratorId`, `dstChainId`, `dstTokenAddr`, `depositWalletAddress`) are validated on construction. Everything else is optional — see the [Configuration Reference](/widget-integration/configuration).

## Methods

| Method                                     | Description                                                                                       |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `mount(container)`                         | Mount the iframe into an `HTMLElement` or CSS selector.                                           |
| `updateConfig(partial)`                    | Patch any config field live via `postMessage` (no reload).                                        |
| `updateStyles(styles)`                     | Update only the [styling](/widget-integration/styling).                                           |
| `updateCustomContractCalls(calls)`         | Replace the [custom calls](/widget-integration/custom-contract-calls).                            |
| `getConfig()`                              | Return the current config object.                                                                 |
| `on(name, handler)` / `off(name, handler)` | Subscribe / unsubscribe to [events](/tracking-deposits/widget-events). Use `"*"` for all.         |
| `notifyWidgetOpened()`                     | Signal the widget it's now visible (see [deferred auth](#preloading-and-deferred-authorization)). |
| `destroy()`                                | Remove the iframe and clean up listeners.                                                         |

### Updating configuration live

Config changes are sent to the widget over `postMessage` — the iframe never reloads:

```typescript
swapper.updateConfig({
  depositWalletAddress: "0xNewAddress...",
  dstChainId: "1",
});
```

This is how you react to your own app's state — e.g. the user switched the account they're funding.

## Listening for events

Pass an `onEvent` callback (equivalent to `on("*", …)`), or register handlers on the instance. Full details in [Widget Events](/tracking-deposits/widget-events).

```typescript
import { SwapperIframe, WidgetEventName } from "@swapper-finance/deposit-sdk";
import type { TransactionSuccessPayload } from "@swapper-finance/deposit-sdk";

const swapper = new SwapperIframe({ /* ...config */ });

swapper.on(WidgetEventName.TRANSACTION_SUCCESS, (event) => {
  const payload = event.data as TransactionSuccessPayload;
  console.log("Deposit complete:", payload.txHash, payload.depositOption);
});
```

## Preloading and deferred authorization

To make the first open instant, build the iframe hidden and let it load in the background. Set `deferSmartWalletAuth` so authorization waits until the widget is actually revealed, then signal it with `notifyWidgetOpened()`:

```typescript
const iframe = new SwapperIframe({
  // ...config
  deferSmartWalletAuth: true,
});

// when your UI actually reveals the widget:
iframe.notifyWidgetOpened();
```

`SwapperModal` and `SwapperEmbed` handle this pattern for you — see [Modal & Inline Embed](/widget-integration/modal-and-embed).

## Iframe options

Beyond the shared config, `SwapperIframe` accepts a few embed-only options:

| Option                 | Description                                                                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `container`            | `HTMLElement` or selector to mount into.                                                                                                                                           |
| `iframeUrl`            | Override the widget host. Default: `https://deposit.swapper.finance/`.                                                                                                             |
| `iframeAttributes`     | Extra iframe attributes: `width`, `height`, `minWidth`, `borderRadius`, `title`, `allow`, `sandbox`, …                                                                             |
| `flexibleHeight`       | Opt in to the widget's auto-sizing home page; the SDK animates the iframe to the requested height and the widget emits [`resize`](/tracking-deposits/widget-events#resize) events. |
| `deferSmartWalletAuth` | Load everything but hold off creating a smart-wallet authorization until `notifyWidgetOpened()`.                                                                                   |

## Connecting a wallet

If your app already has the user's wallet connected, you can hand the widget a signer so the user doesn't reconnect. See [Connecting a Wallet](/widget-integration/connecting-a-wallet).

## Cleaning up

Always `destroy()` when your component unmounts to remove the iframe and detach the `message` listeners:

```typescript
swapper.destroy();
```

## Next

* Popup or in-place reveal → [Modal & Inline Embed](/widget-integration/modal-and-embed)
* Every config field → [Configuration Reference](/widget-integration/configuration)
* Theme it → [Styling & Theming](/widget-integration/styling)


# Modal & Inline Embed

Two presentations built on top of [`SwapperIframe`](/widget-integration/iframe-sdk): a centered **modal** popup, and an **inline embed** that reveals a preloaded widget over a placeholder for an instant open. Both live in `@swapper-finance/deposit-sdk`.

## Modal — `SwapperModal` / `openSwapperModal`

A modal overlay with a backdrop, CSS animations, escape / click-outside to close, and body-scroll locking.

### Quick open

```typescript
import { openSwapperModal } from "@swapper-finance/deposit-sdk";

const modal = openSwapperModal({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
  onClose: () => console.log("Modal closed"),
});

// close programmatically
modal.close();
```

### The class API

For more control, use `SwapperModal` directly:

```typescript
import { SwapperModal } from "@swapper-finance/deposit-sdk";

const modal = new SwapperModal({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
  onClose: () => console.log("closed"),
});

modal.open();
modal.isModalOpen();      // → boolean
modal.getIframe();        // → underlying SwapperIframe
modal.updateConfig({ /* patch */ });
modal.close();            // hide (widget stays loaded), route back to home
modal.destroy();          // full teardown
```

`close()` **hides** the modal but keeps the widget loaded for an instant reopen, and tells the widget to return to its home screen so the next `open()` starts fresh rather than resuming the previous selection.

### `modalStyle`

Style the modal chrome (distinct from the [widget's own styling](/widget-integration/styling)):

| Field          | Default             | Description          |
| -------------- | ------------------- | -------------------- |
| `width`        | `"450px"`           | Modal width.         |
| `height`       | `"560px"`           | Modal height.        |
| `overlayColor` | `"rgba(0,0,0,0.7)"` | Backdrop color.      |
| `borderRadius` | `"30px"`            | Modal corner radius. |
| `zIndex`       | `10000`             | Stacking order.      |

The widget renders its own close button inside the modal (and the modal closes on its `close_request`), so there is no separate modal-level close-button option — style the button via the [widget theme](/widget-integration/styling).

### Flexible height

The modal auto-enables the widget's flexible home-page height: the home screen scales to its content (instead of the fixed 560px), and the modal smoothly animates to match (clamped to 90% of the viewport). Users with `prefers-reduced-motion` get an instant snap. Nothing to configure.

## Preloading the modal

Build the modal hidden and load the widget in the background so the first `open()` is instant:

```typescript
import { preloadSwapperModal } from "@swapper-finance/deposit-sdk";

// when your page mounts
const modal = preloadSwapperModal({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
});

// later, on user action — opens instantly
button.addEventListener("click", () => modal.open());
```

Equivalent to `new SwapperModal({ ..., preload: true })` or calling `modal.preload()`.

If something changed between preload and open (say the deposit address), pass a patch to `open()` — it's applied to the live widget right before it becomes visible:

```typescript
modal.open({ depositWalletAddress: "0x...newAddress" });
```

`updateConfig(patch)` and `updateSigner(signer)` are also exposed directly and are safe to call before the modal has been built.

{% hint style="info" %}
Preload loads the widget but holds smart-wallet authorization until the first `open()`.
{% endhint %}

## Inline embed — `SwapperEmbed`

For widgets that live in the page layout (not a modal) but should still open instantly. The iframe loads immediately on a hidden `position: fixed` host; `showIn(target)` moves the host over the target element's box, so the already-loaded widget appears there with no reload.

```typescript
import { preloadSwapperEmbed } from "@swapper-finance/deposit-sdk";

// when your page mounts — starts loading in the background
const embed = preloadSwapperEmbed({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
});

// later — the widget appears over the placeholder instantly
button.addEventListener("click", () => embed.showIn("#widget-placeholder"));

embed.hide();     // widget stays loaded, routes back to home
embed.destroy();  // full teardown
```

The target is a **placeholder**: size it to the box the widget should occupy (e.g. `450px × 560px`). The host mirrors the target's drawable box — rect, corner radii, border widths, and stacking position — and follows it across scrolling, resizes, and size changes. The iframe is never reparented in the DOM (that would force a reload).

Pass `flexibleHeight: true` to let the home page auto-scale; the embed animates the *placeholder's* height so your page layout reflows with it.

Like the modal, smart-wallet authorization is deferred until the first `showIn()`.

## Which one?

* **A button that pops a dialog** → `openSwapperModal`.
* **The same, but instant** → `preloadSwapperModal`.
* **A widget that sits in your layout and opens instantly** → `preloadSwapperEmbed`.
* **Full manual control over an inline iframe** → [`SwapperIframe`](/widget-integration/iframe-sdk).


# Configuration Reference

Every option the widget accepts through the [iframe SDK](/widget-integration/iframe-sdk) (`SwapperConfig`). The four required fields are always needed; everything else is optional and noted per field.

## Required

| Field                  | Type     | Description                                                                                              |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `integratorId`         | `string` | Your registered integrator id. Scopes the session and links your [webhook](/tracking-deposits/webhooks). |
| `dstChainId`           | `string` | Destination chain id (e.g. `"8453"`).                                                                    |
| `dstTokenAddr`         | `string` | Destination token address. Use `0x0000000000000000000000000000000000000000` for native.                  |
| `depositWalletAddress` | `string` | Wallet that receives the destination token.                                                              |

```typescript
const config = {
  integratorId: "your-integrator-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
};
```

## Optional

### `styles`

Two-tier theming (`themeMode` + `componentStyles`). See [Styling & Theming](/widget-integration/styling) for the full property list.

```typescript
styles: {
  themeMode: "dark",
  componentStyles: { primaryColor: "#836FFF", backgroundColor: "#111111" },
}
```

### `supportedDepositOptions`

Restrict which funding methods appear on the home screen, as an array of keys. When omitted, the default set is shown (opt-in options excluded).

| Key                       | Method                                              |
| ------------------------- | --------------------------------------------------- |
| `"transferCrypto"`        | [Transfer crypto](/deposit-methods/transfer-crypto) |
| `"depositWithCash"`       | [Pay with card](/deposit-methods/fiat-onramp)       |
| `"walletDeposit"`         | [Wallet deposit](/deposit-methods/wallet-deposit)   |
| `"depositFromPolymarket"` | Opt-in source flow                                  |
| `"depositFromPerps"`      | Opt-in source flow                                  |

```typescript
supportedDepositOptions: ["transferCrypto", "depositWithCash"],
```

If only fiat-capable options apply to a destination, the widget narrows the list automatically.

### `actionLabel`

Wording mode across the UI: `"buy"` (default — "Buy with Fiat", "Buy with QR") or `"deposit"` ("Deposit with Fiat", …).

```typescript
actionLabel: "deposit",
```

### `minDepositUsd` / `maxDepositUsd`

Clamp deposit size in **USD**, validated against the USD value of a quote's destination (received) amount.

* `minDepositUsd` overrides the integrator-config minimum; in the transfer flow it's still floored by a gas-based estimate so fees are always covered.
* `maxDepositUsd` has no integrator-config equivalent.

```typescript
minDepositUsd: 5,
maxDepositUsd: 1000,
```

### `customContractCalls`

Extra on-chain actions to run after the swap (approve → stake → transfer, etc.). See [Custom Contract Calls](/widget-integration/custom-contract-calls).

### `wallet`

Hand the widget a signer / wallet handlers so a user who's already connected in your app doesn't reconnect. See [Connecting a Wallet](/widget-integration/connecting-a-wallet).

### `onEvent`

A wildcard event handler called for every [widget event](/tracking-deposits/widget-events) — shorthand for `on("*", …)`.

```typescript
onEvent: (event) => console.log(event.type, event.data),
```

## Iframe-SDK-only options

Passed to `SwapperIframe` / `SwapperModal` / `SwapperEmbed` (not the direct widget):

| Option                 | Type                    | Description                                                                                   |
| ---------------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
| `container`            | `HTMLElement \| string` | Where to mount (`SwapperIframe`).                                                             |
| `iframeUrl`            | `string`                | Override the widget host. Default `https://deposit.swapper.finance/`.                         |
| `iframeAttributes`     | `object`                | Extra iframe attributes (`width`, `height`, `sandbox`, …).                                    |
| `flexibleHeight`       | `boolean`               | Opt in to auto-sizing home page + [`resize`](/tracking-deposits/widget-events#resize) events. |
| `deferSmartWalletAuth` | `boolean`               | Load everything but defer smart-wallet auth until `notifyWidgetOpened()`.                     |

## Widget-only props

Set on the hosted widget internally by the iframe host — you don't pass these directly, but they're documented here for reference:

| Prop                   | Type      | Description                                                                                                                                 |
| ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `closable`             | `boolean` | Show a close affordance inside the widget.                                                                                                  |
| `webviewMode`          | `boolean` | Render the fiat on-ramp step as a plain user-clickable link (no `window.open`) so iOS/Android WebViews can hand off to the system browser.  |
| `flexibleHeight`       | `boolean` | Auto-scale the home page; the host is expected to honor `resize` events.                                                                    |
| `deferSmartWalletAuth` | `boolean` | Hold off creating the smart-wallet authorization until signaled visible.                                                                    |
| `integrationError`     | `string`  | Pre-set error — the widget skips its chain fetch and renders an error view (used by iframe hosts to surface URL-param validation failures). |

## Updating config at runtime

All of the above (except transport-level iframe options) can be patched live — the iframe never reloads:

```typescript
swapper.updateConfig({ dstChainId: "1", depositWalletAddress: "0x…" });
swapper.updateStyles({ themeMode: "light" });
swapper.updateCustomContractCalls([ /* … */ ]);
```


# Styling & Theming

The widget is built to feel native to your product. Theming is a **two-tier system**, applied in order of increasing priority:

1. **Theme mode** — pick light or dark.
2. **Component styles** — override individual brand colors and the width.

Component styles win over theme mode. Pass both, or just what you need.

## The `styles` object

```typescript
type SwapperStyles = {
  themeMode?: "light" | "dark";
  componentStyles?: ComponentStyles;
};
```

### Basic theme selection

```typescript
const styles = { themeMode: "dark" }; // or "light" (default)
```

### With overrides

```typescript
const styles = {
  themeMode: "dark",
  componentStyles: {
    primaryColor: "#FF6B35",
    backgroundColor: "#1A1A1A",
    surfaceColor: "#222222",
    textColor: "#E8E8E8",
    primaryButtonTextColor: "#FFFFFF",
    width: "100%",
  },
};
```

## `componentStyles` properties

| Property                 | Purpose                                             |
| ------------------------ | --------------------------------------------------- |
| **Layout**               |                                                     |
| `width`                  | Widget width (default `450px`).                     |
| **Brand & surfaces**     |                                                     |
| `primaryColor`           | Main brand color — buttons, links, primary accents. |
| `backgroundColor`        | Widget background.                                  |
| `surfaceColor`           | Cards and inputs.                                   |
| **Typography**           |                                                     |
| `textColor`              | Main text.                                          |
| `primaryButtonTextColor` | Text on primary buttons.                            |

## Applying styles

**Iframe SDK / modal / embed** — pass `styles` in the config, or update live:

```typescript
const swapper = new SwapperIframe({ /* ...config */, styles: { themeMode: "dark" } });

// change it later without reloading:
swapper.updateStyles({
  themeMode: "light",
  componentStyles: { primaryColor: "#836FFF" },
});
```

## Under the hood: CSS variables

Inside the widget, styles map to CSS custom properties applied at the widget root — `--swapper-primary-color`, `--swapper-background-color`, `--swapper-text-color`, `--swapper-width`, and friends — with dark mode toggled via a class. Passing `componentStyles` sets these for you.

## Modal chrome vs. widget theme

For the [modal](/widget-integration/modal-and-embed), `styles` themes the **widget content**, while `modalStyle` controls the **modal shell** (overlay color, modal radius, width/height, z-index). They're independent — style both to match your app.

{% hint style="info" %}
Set `width: "100%"` in `componentStyles` to let the widget fill a responsive container, then constrain the container itself in your layout.
{% endhint %}


# Connecting a Wallet

By default the widget manages wallet connection itself (it bundles a full wallet UI). But if your app **already has the user's wallet connected**, you can hand the widget a signer so the user doesn't connect twice. This applies to the [iframe SDK](/widget-integration/iframe-sdk) via the `wallet` config option.

## Why

For the [transfer-crypto](/deposit-methods/transfer-crypto) flow the widget needs to request transactions and chain switches. Passing your existing connection means:

* no second "Connect Wallet" step for the user;
* transactions are signed by the wallet your app already trusts;
* the widget reflects the account/chain your app is on.

## Option A — pass a signer

The simplest form: give the widget a signer object. The widget **auto-connects with it as soon as it loads** — no second "Connect Wallet" step, and no flag to set (the SDK reads the address and chain from the signer for you).

```typescript
import { SwapperIframe } from "@swapper-finance/deposit-sdk";

const swapper = new SwapperIframe({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x2A0…28A",
  wallet: { signer }, // an ethers/viem-compatible signer — auto-connected on load
});
```

The SDK ships adapters that normalize common wallet SDKs into a `SwapperSigner` — an object exposing `getAddress()`, `getChainId()`, `sendTransaction()`, and `switchChain()`.

## Option B — provide handlers

If you'd rather keep signing entirely in your app, provide callbacks. The widget calls them when it needs a transaction sent or a chain switched, and you return the result:

```typescript
const swapper = new SwapperIframe({
  // ...required config
  wallet: {
    onTransactionRequest: async (tx) => {
      // tx: { to, data?, value?, gasLimit? }
      const hash = await myWallet.sendTransaction(tx);
      return { hash };
    },
    onChainSwitchRequest: async (chainId) => {
      await myWallet.switchChain(chainId);
    },
    autoConnect: { address: "0x…", chainId: 8453, walletName: "metamask" },
  },
});
```

In handler mode, auto-connect needs the connection details (`address` / `chainId`) — there's no signer for the SDK to read them from. Provide `autoConnect` to land the user in the connected state on load; omit it to let them connect inside the widget.

## Updating the signer later

If the user switches accounts in your app, push the new signer to the widget:

```typescript
swapper.updateSigner(newSigner); // also available directly on SwapperModal
```

(`SwapperModal.updateSigner()` is safe to call before the modal is built — handy with [preloading](/widget-integration/modal-and-embed#preloading-the-modal).)

## Under the hood: the wallet message protocol

The connection is bridged across the iframe boundary by `SwapperWalletProvider` using a small `postMessage` protocol (`SWAPPER_WALLET_*` messages):

| Message                                       | Direction     | Meaning                                             |
| --------------------------------------------- | ------------- | --------------------------------------------------- |
| `SWAPPER_WALLET_READY`                        | widget → host | Widget is ready to receive a connection.            |
| `SWAPPER_WALLET_CONNECT`                      | host → widget | Report connected `address` / `chainId`.             |
| `SWAPPER_WALLET_DISCONNECT`                   | host → widget | Wallet disconnected.                                |
| `SWAPPER_WALLET_CHAIN_CHANGED`                | host → widget | Active chain changed.                               |
| `SWAPPER_TX_REQUEST` / `SWAPPER_TX_RESPONSE`  | widget ↔ host | Request a tx be signed; return the hash (or error). |
| `SWAPPER_CHAIN_SWITCH_REQUEST` / `…_RESPONSE` | widget ↔ host | Request a chain switch.                             |

You don't normally touch these directly — the `wallet` config and the adapters handle it. They're documented here so you can debug message flow if needed.

{% hint style="info" %}
Recognized wallet names (for `autoConnect.walletName`) include `metamask`, `rabby`, `coinbase`, `phantom`, `trust`, `okx`, `rainbow`, `walletconnect`, `ledger`, `safe`, and others. The name is a hint for display; the signer / handlers do the real work.
{% endhint %}


# Custom Contract Calls

Custom contract calls let you run **additional on-chain actions after the deposit swap completes** — all in the same user flow. Stake the received tokens, wrap them, add liquidity, deposit into a vault: any contract interaction, chained.

The widget executes the flow as: **swap → deliver → your calls, in sequence**. If any call reverts, the whole thing reverts.

## The `ContractCall` object

Each call is:

```typescript
interface ContractCall {
  callType: ContractCallType; // how to handle amounts
  target: string;             // contract to call
  value: string;              // native value in wei (usually "0")
  callData: string;           // ABI-encoded function call
  payload: string;            // extra data for balance encoding
}
```

Pass an array as `customContractCalls`:

```typescript
import { SwapperIframe, ContractCallType } from "@swapper-finance/deposit-sdk";

const swapper = new SwapperIframe({
  integratorId: "your-id",
  dstChainId: "1",
  dstTokenAddr: "0x…",        // token the calls operate on
  depositWalletAddress: "0x…",
  customContractCalls: [ /* calls */ ],
});
```

## Call types

`ContractCallType` controls how the executor fills in amounts — because the exact post-swap balance isn't known when you build the calls.

| Type                    | Value | Behavior                                                                                                                                                                                                       |
| ----------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT`               | `0`   | Use the exact amounts encoded in `callData`; nothing is rewritten. `payload` is `"0x"`.                                                                                                                        |
| `FULL_TOKEN_BALANCE`    | `1`   | Overwrite one argument of `callData` with the **full token balance** at execution time. `payload` names the token and which argument to overwrite — see [Balance payload encoding](#balance-payload-encoding). |
| `FULL_NATIVE_BALANCE`   | `2`   | Send the **entire native balance** as the call's `value`. `payload` is `"0x"` — nothing in `callData` is rewritten.                                                                                            |
| `COLLECT_TOKEN_BALANCE` | `3`   | Book-keeping step that snapshots a token's balance for a later call. `payload` is **only** the token address; `target`/`callData` are empty.                                                                   |

## Balance payload encoding

`FULL_TOKEN_BALANCE` is the type you'll reach for most, and its `payload` is the part that trips people up. The rule:

```ts
payload = abiCoder.encode(["address", "uint256"], [tokenAddress, amountArgIndex]);
```

**`amountArgIndex` is the zero-indexed position of the `amount` argument in&#x20;*****your*****&#x20;target function's signature** — i.e. which 32-byte word of the calldata the executor overwrites with the token balance. It is **not** a fixed constant, and it is unrelated to the placeholder value you put in `callData`. Compute it from your own function:

| Function                                                                           | `amount` is argument… | `amountArgIndex` |
| ---------------------------------------------------------------------------------- | --------------------- | ---------------- |
| `approve(address spender, uint256 amount)`                                         | 2nd                   | `1`              |
| `transfer(address recipient, uint256 amount)`                                      | 2nd                   | `1`              |
| `deposit(uint256 _amount, bool _shouldQueue, bytes[] _data)`                       | 1st                   | `0`              |
| `depositFor(address currency, address forAddress, uint256 amount, uint256 broker)` | 3rd                   | `2`              |

In `callData`, put a placeholder (`0`) at that same argument — the executor overwrites it with the real balance at runtime. **A wrong `amountArgIndex` patches the wrong word of your calldata and the call will almost certainly revert** (e.g. using `0` for `depositFor` would overwrite the `currency` address with a number). The index equals the argument position whenever every preceding argument is a value type — the usual case; functions with dynamic-type args before the amount are the exception.

The other two balance types are shaped **differently** — don't copy the `(address, uint256)` payload onto them:

```ts
// FULL_NATIVE_BALANCE — no payload; the full native balance is sent as `value`
{ callType: ContractCallType.FULL_NATIVE_BALANCE, target, value: "0", callData, payload: "0x" }

// COLLECT_TOKEN_BALANCE — payload is just the token address (no index)
{
  callType: ContractCallType.COLLECT_TOKEN_BALANCE,
  target: "0x0000000000000000000000000000000000000000",
  value: "0",
  callData: "0x",
  payload: abiCoder.encode(["address"], [tokenAddress]),
}
```

## ERC-20 helpers

The SDK ships helpers so you don't hand-encode common ERC-20 ops.

**Exact amount:**

```typescript
import { approve, transfer, transferFrom } from "@swapper-finance/deposit-sdk";

approve(tokenAddress, spender, amount);
transfer(tokenAddress, recipient, amount);
transferFrom(tokenAddress, from, to, amount);
```

**Full balance** (most common after a swap, when the amount isn't known ahead of time):

```typescript
import { approveBalance, transferBalance } from "@swapper-finance/deposit-sdk";

approveBalance(tokenAddress, spender);   // approve the whole balance
transferBalance(tokenAddress, recipient); // transfer the whole balance
```

Both helpers target standard ERC-20 `approve`/`transfer`, where `amount` is argument index `1` — a handy reference for the rule above. For a non-standard target function, build the `payload` with the exported helper instead of hand-rolling the tuple (harder to misread as "always 0"):

```typescript
import { encodeBalancePayload } from "@swapper-finance/deposit-sdk";

// depositFor(address currency, address forAddress, uint256 amount, uint256 broker)
payload: encodeBalancePayload(tokenAddress, 2); // amount is argument index 2
```

## Worked example: swap → stake → return receipt

Deposit any token, swap to POL, approve the staking pool, stake the full balance, and send the staked receipt token to the user:

```typescript
import {
  openSwapperModal,
  ContractCallType,
  approveBalance,
  transferBalance,
} from "@swapper-finance/deposit-sdk";
import { ethers } from "ethers";

const stakeInterface = new ethers.utils.Interface([
  "function deposit(uint256 _amount, bool _shouldQueue, bytes[] _data)",
]);
const abiCoder = new ethers.utils.AbiCoder();

openSwapperModal({
  integratorId: "your-integrator-id",
  dstChainId: "1",
  dstTokenAddr: "0x455…3F6", // POL
  depositWalletAddress: "0x928…8da",
  customContractCalls: [
    // 1. Approve the staking pool to pull POL
    approveBalance(
      "0x455…3F6", // POL
      "0xCfa…d67", // pool
    ),

    // 2. Stake the full POL balance
    {
      callType: ContractCallType.FULL_TOKEN_BALANCE,
      target: "0xCfa…d67",
      value: "0",
      // amountArgIndex is 0 ONLY because `_amount` is deposit()'s 1st argument.
      // For e.g. transfer(address, uint256) it would be 1 — see
      // "Balance payload encoding" above. Don't copy the 0 blindly.
      callData: stakeInterface.encodeFunctionData("deposit", ["0", true, ["0x"]]),
      payload: abiCoder.encode(
        ["address", "uint256"],
        ["0x455…3F6", "0"], // [token, amountArgIndex]
      ),
    },

    // 3. Send the staked receipt token to the user
    transferBalance(
      "0x2ff…753", // stPOL
      "0x928…8da", // recipient
    ),
  ],
});
```

## Updating calls at runtime

```typescript
swapper.updateCustomContractCalls([ approveBalance("0x…", "0x…") /* … */ ]);
// or
swapper.updateConfig({ customContractCalls: [ /* … */ ] });
```

## Guidelines

* **Order matters.** Calls run sequentially — approvals before the operations that need them.
* **Get `amountArgIndex` right** for `FULL_TOKEN_BALANCE` — it's the position of the `amount` argument in *your* function, not always `0`. See [Balance payload encoding](#balance-payload-encoding).
* **All-or-nothing.** A revert anywhere rolls back the entire sequence.
* **Test with small amounts first.** Chained calls are easy to get subtly wrong; verify the whole sequence before relying on it.
* **Gas** is estimated automatically for the chain.


# Overview

The widget's home screen offers several ways for a user to fund your destination: three methods shown by default, plus opt-in source flows you enable explicitly. This section explains what each one does, what the user experiences, and what your app receives.

You control which appear with [`supportedDepositOptions`](/widget-integration/configuration#supporteddepositoptions); omit it to show the default set.

## The default methods

| Method                                              | Key               | User pays with                        | Best when                         |
| --------------------------------------------------- | ----------------- | ------------------------------------- | --------------------------------- |
| [Pay with Card](/deposit-methods/fiat-onramp)       | `depositWithCash` | Card / Apple Pay / bank (fiat)        | Users have no crypto yet.         |
| [Transfer Crypto](/deposit-methods/transfer-crypto) | `transferCrypto`  | A one-tap smart-wallet deposit        | Lowest-friction on-chain funding. |
| [Wallet Deposit](/deposit-methods/wallet-deposit)   | `walletDeposit`   | Tokens in their own wallet, any chain | Crypto-native users.              |

All three converge on the same outcome: your `dstTokenAddr` on `dstChainId`, delivered to `depositWalletAddress`.

## Opt-in source flows

Two additional funding sources, **off by default** — enable them explicitly in [`supportedDepositOptions`](/widget-integration/configuration#supporteddepositoptions). Each pulls the user's balance from that platform and swaps/bridges it to your destination token, much like [Wallet Deposit](/deposit-methods/wallet-deposit) but sourced from a specific platform account rather than a connected wallet.

| Method                  | Key                     | Source                                        |
| ----------------------- | ----------------------- | --------------------------------------------- |
| Deposit from Polymarket | `depositFromPolymarket` | The user's Polymarket USDC balance            |
| Deposit from Perps      | `depositFromPerps`      | The user's balance on a supported perps venue |

## How they map to a deposit's `method`

When a deposit completes, its record (in [webhooks](/tracking-deposits/webhooks)) carries a `method` field. It relates to the widget option like so:

| Widget option     | Deposit `method`                                         |
| ----------------- | -------------------------------------------------------- |
| `depositWithCash` | `onramp` (and, if swept onward, a linked `smart_wallet`) |
| `transferCrypto`  | `smart_wallet`                                           |
| `walletDeposit`   | `dex_swap`                                               |

The onramp-plus-sweep case produces **two linked deposits** — see [Webhooks → Linking](/tracking-deposits/webhooks#linking-onramp--wallet-deposit).

## Amounts and limits

* Deposit size can be clamped in USD with [`minDepositUsd` / `maxDepositUsd`](/widget-integration/configuration#mindepositusd--maxdepositusd).
* The wallet-deposit flow always floors the minimum by a gas-based estimate so network fees are covered.
* All reported amounts are [base-unit strings](/getting-started/core-concepts#amounts-are-base-unit-strings).

## Choosing what to show

* **Consumer app, mainstream users** → lead with `depositWithCash`; the one-tap `transferCrypto` is also low-friction. Keep `walletDeposit` for crypto-native users who prefer their own wallet.
* **DeFi / on-chain product** → `walletDeposit` and `transferCrypto`.
* **Not sure** → show the default set and let users pick.
* **Built on Polymarket or a perps venue?** → enable the matching opt-in source flow so users can fund straight from their platform balance.

Read the per-method pages for the details:

* [Pay with Card](/deposit-methods/fiat-onramp)
* [Transfer Crypto](/deposit-methods/transfer-crypto)
* [Wallet Deposit](/deposit-methods/wallet-deposit)


# Pay with Card

**Key:** `depositWithCash` · **Deposit `method`:** `onramp`

The fiat on-ramp lets users fund with a **card, Apple Pay, or bank transfer** — no crypto required. Swapper's on-ramp partner converts cash to crypto, then that crypto is routed and swapped to your destination token.

## What the user experiences

1. Chooses **Pay with Card** (labeled "Buy with Fiat" by default, or "Deposit with Fiat" if you set [`actionLabel: "deposit"`](/widget-integration/configuration#actionlabel)).
2. Enters an amount and picks a currency and payment method available in their region.
3. Completes payment with the provider (hosted, PCI-compliant).
4. The purchased crypto is delivered and swapped to your `dstTokenAddr`, which arrives at `depositWalletAddress`.

Availability of currencies and payment methods is determined by the provider and the user's geolocation.

## Two settlement shapes

Where the purchased crypto lands determines how the deposit is reported:

* **Straight to an external wallet** — the on-ramp delivers directly. The deposit is fully done at `completed`.
* **Into a Swapper smart wallet, then swept onward** — when the destination needs a further swap/bridge, the on-ramp funds one of our smart wallets, and the onward swap is tracked as a **separate** `smart_wallet` deposit. Final delivery is marked by that second deposit's completion.

In the second case you receive **two** webhook events — one `onramp`, one `smart_wallet` — correlated on-chain. Don't double-count them. See [Webhooks → Linking](/tracking-deposits/webhooks#linking-onramp--wallet-deposit).

## What your app receives

A [`transaction.completed` webhook](/tracking-deposits/webhooks) whose `deposit.method` is `onramp`, with `providerData.type = "onramp"`:

```json
{
  "type": "onramp",
  "transactionId": "a1b2c3d4-…",
  "fiatCurrency": "USD",
  "fiatAmount": "11.50",
  "paymentMethod": "CARD"
}
```

The on-ramp deposit also carries `depositTxHash` — the on-chain tx that delivered the crypto — which is the key used to [link](/tracking-deposits/webhooks#linking-onramp--wallet-deposit) it to a follow-on smart-wallet swap.

In the browser you also get a [`transaction_success` widget event](/tracking-deposits/widget-events) with `depositOption: "depositWithCash"`.

## Mobile WebViews

If you embed the widget inside a native mobile WebView (e.g. Ionic/Capacitor), set [`webviewMode: true`](/widget-integration/configuration#widget-only-props). The on-ramp step then renders a plain user-clickable link (instead of calling `window.open(..., "_blank")`), so iOS/Android WebViews can hand the payment URL to the system browser.

## Enabling it

```typescript
openSwapperModal({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x…",
  supportedDepositOptions: ["depositWithCash"], // show only the on-ramp
  actionLabel: "buy",
});
```


# Transfer Crypto

**Key:** `transferCrypto` · **Deposit `method`:** `smart_wallet`

The transfer-crypto flow uses a per-user **smart wallet** (account abstraction) to make funding a low-friction, near-one-tap experience — and to enable powerful patterns like automatically sweeping an on-ramp payment onward into a swap.

## What it is

Each user gets a smart contract wallet (deployed via ZeroDev / EIP-7702) that Swapper can operate on their behalf under tightly scoped permissions (session keys, spending limits). This lets the widget:

* deposit and swap in a single authorized action, rather than a manual approve-then-swap dance;
* accept funds that arrive at the smart wallet (e.g. from a [card on-ramp](/deposit-methods/fiat-onramp)) and **sweep them onward** into your destination token automatically.

The authorization is scoped and bounded — it lets Swapper execute the swap the user asked for, not move funds anywhere else.

## What the user experiences

1. Chooses **Transfer Crypto**.
2. Authorizes the smart wallet (one signature/setup step).
3. Funds it; the deposit and swap execute and the destination token is delivered to `depositWalletAddress`.

## What your app receives

A [`transaction.completed` webhook](/tracking-deposits/webhooks) whose `deposit.method` is `smart_wallet`, with `providerData.type = "smart_wallet"`:

```json
{
  "type": "smart_wallet",
  "authorizationId": "0x…-1-1782205341782",
  "executionId": "1782652045143-0x…",
  "smartWalletAddress": "0xbf6…ac0"
}
```

The smart-wallet deposit carries `fundingTxs` — every on-chain tx that funded the swap. When the funds came from a card on-ramp, one of those funding txs equals the on-ramp deposit's `depositTxHash`; that's how the two deposits are [linked](/tracking-deposits/webhooks#linking-onramp--wallet-deposit). It may also carry `deposited` (what the user originally put in) alongside `source` (what actually entered the swap).

In the browser you also get a [`transaction_success` widget event](/tracking-deposits/widget-events) with `depositOption: "transferCrypto"`.

## On-ramp → smart-wallet sweep

The headline pattern: a user pays by card, the crypto lands in their smart wallet, and Swapper sweeps it into your destination token — a fully cash-to-your-token path with no manual crypto steps. Operationally this produces two linked deposits (`onramp` + `smart_wallet`); make sure your accounting counts the flow **once**. See [Webhooks → Linking](/tracking-deposits/webhooks#linking-onramp--wallet-deposit).

## Debugging a stuck deposit

If a smart-wallet deposit never executes, the usual suspects are a stuck delegation job, a sponsorship failure, an EIP-7702 authorization problem, or a userOp that reverts in simulation. Gather the `authorizationId`, `executionId`, `smartWalletAddress`, and funding tx hash and open a ticket in [Discord](https://discord.gg/y8eevERxBz).

## Enabling it

```typescript
openSwapperModal({
  integratorId: "your-id",
  dstChainId: "8453",
  dstTokenAddr: "0x833…913",
  depositWalletAddress: "0x…",
  supportedDepositOptions: ["transferCrypto"],
});
```


# Wallet Deposit

**Key:** `walletDeposit` · **Deposit `method`:** `dex_swap`

The wallet-deposit flow lets a crypto-native user fund straight from **their own wallet**, with tokens they already hold on **any supported chain**. Swapper finds the best route, swaps, and bridges as needed to deliver your destination token.

## What the user experiences

1. Chooses **Wallet Deposit**.
2. Connects a wallet — or, if your app already passed one via [`wallet`](/widget-integration/connecting-a-wallet), reuses that connection with no extra step.
3. Picks a source chain and token they hold, and an amount.
4. Reviews the quote — estimated output, minimum after slippage, and fees.
5. Approves (for ERC-20 sources) and signs the swap. Cross-chain routes bridge automatically.
6. The destination token arrives at `depositWalletAddress`.

Sources span multiple ecosystems (EVM, Solana, and more). Which source chains can reach a given destination is determined by Swapper's routing engine.

## How routing works

Under the hood the widget quotes the best route for the source → destination pair, then drives the user's wallet through the transactions it needs:

* An `approve` (when the source is an ERC-20 and not yet approved) followed by a `swap`, or a single Solana `VersionedTransaction`.
* Cross-chain routes bridge automatically.
* Network and token fees are shown in the quote before the user signs.

You don't implement any of this — the widget drives the wallet for the user.

## Slippage, minimums, and fees

* The quote shows a **minimum received** after slippage; settlement never delivers less.
* [`minDepositUsd`](/widget-integration/configuration#mindepositusd--maxdepositusd) is honored, but this flow additionally **floors** the minimum by a gas-based estimate so the user can always cover network fees.
* Native-token sources skip the approve step.

## What your app receives

A [`transaction.completed` webhook](/tracking-deposits/webhooks) whose `deposit.method` is `dex_swap`, with `providerData.type = "dex_swap"` (carrying the route id and router address when available), plus the source and destination assets and tx hashes.

In the browser you also get a [`transaction_success` widget event](/tracking-deposits/widget-events) with `depositOption: "walletDeposit"`, including `txHash`, `explorerUrl`, `tokenSymbol`, `chainId`, and `amountReceived` when known.

## Chaining actions after the swap

Because the swap ends by delivering a token, you can attach [custom contract calls](/widget-integration/custom-contract-calls) to stake, wrap, or otherwise put the funds to work in the same flow.

## Enabling it

```typescript
openSwapperModal({
  integratorId: "your-id",
  dstChainId: "42161",
  dstTokenAddr: "0x0000000000000000000000000000000000000000", // native ETH
  depositWalletAddress: "0x…",
  supportedDepositOptions: ["walletDeposit"],
});
```


# Widget Events

The widget emits structured events via `window.postMessage()` from inside the iframe to your parent page. This lets you react to what the user does — most importantly, a completed deposit — in real time, with no polling.

Widget events are for **in-browser UX**. For reliable server-side accounting, pair them with [webhooks](/tracking-deposits/webhooks).

## The event envelope

Every event posted from the widget has this shape:

```typescript
interface SwapperWidgetEvent {
  type: "SWAPPER_EVENT";       // discriminator — filter on this
  version: "1.0";              // protocol version
  name: WidgetEventName;       // e.g. "transaction_success"
  timestamp: string;           // ISO 8601
  payload: WidgetEventPayload; // event-specific data
}
```

| Field       | Description                                                         |
| ----------- | ------------------------------------------------------------------- |
| `type`      | Always `"SWAPPER_EVENT"` — use it to ignore unrelated postMessages. |
| `version`   | Protocol version (currently `"1.0"`).                               |
| `name`      | Event name (see [below](#event-types)).                             |
| `timestamp` | When it was emitted (ISO 8601).                                     |
| `payload`   | Event-specific data.                                                |

## Three ways to listen

### `onEvent` (recommended)

Pass a callback in the config — it receives every event:

```typescript
import { SwapperIframe, WidgetEventName } from "@swapper-finance/deposit-sdk";
import type { TransactionSuccessPayload } from "@swapper-finance/deposit-sdk";

const swapper = new SwapperIframe({
  // ...required config
  onEvent: (event) => {
    if (event.type === WidgetEventName.TRANSACTION_SUCCESS) {
      const payload = event.data as TransactionSuccessPayload;
      console.log("Deposit complete:", payload.txHash);
    }
  },
});
```

Works with `openSwapperModal` too.

### `on()` / `off()`

Register handlers on the instance; `"*"` catches everything:

```typescript
swapper.on(WidgetEventName.TRANSACTION_SUCCESS, (event) => { /* … */ });
swapper.on("*", (event) => console.log(event.type, event.data));
swapper.off(WidgetEventName.TRANSACTION_SUCCESS, handler);
```

### Raw `window.addEventListener`

If you're not using the SDK's listeners (or you embed the widget directly), read the messages yourself:

```typescript
import { WIDGET_EVENT_PROTOCOL_VERSION, WidgetEventName } from "@swapper-finance/deposit-sdk";
import type { SwapperWidgetEvent, TransactionSuccessPayload } from "@swapper-finance/deposit-sdk";

window.addEventListener("message", (event: MessageEvent) => {
  if (event.data?.type !== "SWAPPER_EVENT") return;
  const message = event.data as SwapperWidgetEvent;
  if (message.version !== WIDGET_EVENT_PROTOCOL_VERSION) return;

  if (message.name === WidgetEventName.TRANSACTION_SUCCESS) {
    const payload = message.payload as TransactionSuccessPayload;
    console.log("Tx:", payload.txHash, "Explorer:", payload.explorerUrl);
  }
});
```

## Event types

`WidgetEventName` currently defines:

| Name                  | Value                   | Meaning                                                 |
| --------------------- | ----------------------- | ------------------------------------------------------- |
| `TRANSACTION_SUCCESS` | `"transaction_success"` | A deposit completed successfully.                       |
| `RESIZE`              | `"resize"`              | The widget's required height changed (flexible height). |
| `CLOSE_REQUEST`       | `"close_request"`       | The widget is asking the host to close it.              |

### `transaction_success`

Emitted when a deposit completes.

| Field            | Type      | Description                                                               |
| ---------------- | --------- | ------------------------------------------------------------------------- |
| `depositOption`  | `string`  | Flow used: `"walletDeposit"`, `"transferCrypto"`, or `"depositWithCash"`. |
| `txHash`         | `string?` | On-chain transaction hash, when available.                                |
| `explorerUrl`    | `string?` | Block-explorer link for the transaction.                                  |
| `tokenSymbol`    | `string?` | Symbol of the received token (e.g. `"USDC"`).                             |
| `tokenAddress`   | `string?` | Contract address of the received token.                                   |
| `chainId`        | `string?` | Chain id where the token was received.                                    |
| `amountReceived` | `string?` | Human-readable amount received.                                           |

### `resize`

Emitted when [flexible height](/widget-integration/modal-and-embed#flexible-height) is on and the home page's content height changes. Payload: `{ height: number }` (CSS pixels). The modal/embed animates to this automatically; if you host the iframe yourself, resize it to match.

### `close_request`

The widget requests to be dismissed (e.g. a close affordance was tapped). Handle it by closing your modal/container.

## Extensibility

New event names will be added to `WidgetEventName` over time. **Filter by the `name` string** and ignore names you don't recognize — new events can ship without an SDK bump, and old integrations keep working.

## Events vs. webhooks

|             | Widget event           | [Webhook](/tracking-deposits/webhooks) |
| ----------- | ---------------------- | -------------------------------------- |
| Channel     | Browser `postMessage`  | Server-to-server HTTP                  |
| Latency     | Instant                | Seconds                                |
| Reliability | Lost if the tab closes | Retried, signed                        |
| Use for     | Live UI updates        | Crediting accounts (source of truth)   |

Use both.


# Webhooks

Swapper sends a **signed, server-to-server webhook** when a deposit completes. Use it as your reliable source of truth for crediting accounts — unlike a [widget event](/tracking-deposits/widget-events), a webhook isn't lost when the user closes the tab, and it's retried on failure.

## Setup

Webhook delivery is configured on Swapper's side. Swapper registers your endpoint URL against your `integratorId` and issues a **signing secret**. To set or change your endpoint, or to receive your secret, contact [Support](/resources/support).

Your endpoint must be:

* served over **HTTPS**;
* **publicly reachable** from the internet;
* **fast** — return HTTP 2xx within **10 seconds** (slow responses count as a failure and trigger a retry).

```js
app.post("/webhooks/deposit", (req, res) => {
  const { eventId, eventType, data } = req.body;
  const { deposit } = data;
  console.log("Deposit completed:", deposit.id, deposit.txHash);
  res.json({ received: true });
});
```

## The event

`transaction.completed` is the **only** event type. You are notified once, when a deposit completes — there are no `created` / `processing` / `failed` events.

### Top-level fields

| Field               | Type    | Description                            |
| ------------------- | ------- | -------------------------------------- |
| `eventId`           | string  | Unique event id — use for idempotency. |
| `eventType`         | string  | Always `transaction.completed`.        |
| `timestamp`         | string  | ISO 8601 timestamp of the event.       |
| `data.deposit`      | Deposit | The deposit (below).                   |
| `data.integratorId` | string  | Your integrator id.                    |

### Deposit object

| Field                | Type         | Description                                                                    |
| -------------------- | ------------ | ------------------------------------------------------------------------------ |
| `id`                 | string       | Unique deposit id.                                                             |
| `method`             | string       | `onramp` \| `smart_wallet` \| `dex_swap`.                                      |
| `status`             | string       | Lifecycle status — `completed` on this event.                                  |
| `destinationAddress` | string       | Where funds are delivered (see [Linking](#linking-onramp--wallet-deposit)).    |
| `integratorId`       | string       | Your integrator id.                                                            |
| `source`             | Asset        | What went into the swap.                                                       |
| `deposited`          | Asset?       | What the user originally deposited (`smart_wallet`).                           |
| `destination`        | Asset?       | What was delivered on the destination chain.                                   |
| `depositTxHash`      | string?      | **onramp only** — the on-chain tx that delivered crypto into the smart wallet. |
| `fundingTxs`         | string\[]?   | **smart\_wallet only** — every on-chain tx that funded the swap.               |
| `txHash`             | string?      | Source-chain swap tx hash.                                                     |
| `destinationTxHash`  | string?      | Destination-chain tx hash (cross-chain).                                       |
| `createdAt`          | string       | ISO 8601.                                                                      |
| `updatedAt`          | string       | ISO 8601.                                                                      |
| `completedAt`        | string?      | ISO 8601.                                                                      |
| `providerData`       | ProviderData | Method-specific data (below).                                                  |

### Asset

| Field          | Type    | Description                          |
| -------------- | ------- | ------------------------------------ |
| `chainId`      | string  | Chain identifier.                    |
| `tokenAddress` | string  | Token contract address.              |
| `tokenSymbol`  | string? | Token symbol, when known.            |
| `amount`       | string? | Amount in the token's smallest unit. |
| `amountUsd`    | string? | USD value, when known.               |

### ProviderData

Discriminated by `type`:

```ts
type ProviderData =
  | {
      type: "onramp";
      transactionId: string;
      sessionId?: string;
      externalCustomerId?: string;
      customerId?: string;
      fiatCurrency?: string;
      fiatAmount?: string;
      paymentMethod?: string;
      rawStatus?: string;
    }
  | {
      type: "smart_wallet";
      authorizationId: string;
      executionId: string;
      smartWalletAddress: string;
    }
  | {
      type: "dex_swap";
      routeRequestId?: string;
      rawStatus?: string;
      routerAddress?: string;
    };
```

### Example — smart-wallet swap

```json
{
  "eventId": "13de526e-a4b5-468f-808f-7eaeea08fc10",
  "eventType": "transaction.completed",
  "timestamp": "2026-06-28T13:11:08.753Z",
  "data": {
    "deposit": {
      "id": "smart_wallet-1782652045143-0xa0b8...eb48",
      "method": "smart_wallet",
      "status": "completed",
      "destinationAddress": "0x928…8da",
      "integratorId": "your-id",
      "source": {
        "chainId": "1",
        "tokenAddress": "0xa0b8...eb48",
        "amount": "11150662",
        "amountUsd": "11.13"
      },
      "deposited": {
        "chainId": "1",
        "tokenAddress": "0xa0b8...eb48",
        "amount": "11150662",
        "amountUsd": "11.13"
      },
      "destination": {
        "chainId": "36900",
        "tokenAddress": "0x9cb8...71c2",
        "amount": "11139512",
        "amountUsd": "11.14"
      },
      "fundingTxs": ["0x47c2d908...26fb5"],
      "txHash": "0x9f2719ff...ebe9e",
      "destinationTxHash": "0xdddeb0c5...4be6a",
      "createdAt": "2026-06-28T13:07:35.000Z",
      "updatedAt": "2026-06-28T13:11:08.689Z",
      "completedAt": "2026-06-28T13:11:08.689Z",
      "providerData": {
        "type": "smart_wallet",
        "authorizationId": "0xbf64...2ac0-1-1782205341782",
        "executionId": "1782652045143-0xa0b8...eb48",
        "smartWalletAddress": "0xbf6…ac0"
      }
    },
    "integratorId": "your-id"
  }
}
```

## Verifying the signature

Every request includes an `X-Webhook-Signature` header: an **HMAC-SHA256** signature of the **raw JSON request body**, keyed with your integrator secret, **Base64**-encoded. Verify it before trusting a payload.

```js
const crypto = require("crypto");

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload) // the raw request body string
    .digest("base64");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post("/webhooks/deposit", (req, res) => {
  const signature = req.headers["x-webhook-signature"];
  const payload = JSON.stringify(req.body); // prefer the raw body if available
  const secret = process.env.SWAPPER_WEBHOOK_SECRET;

  if (!verifyWebhookSignature(payload, signature, secret)) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  res.json({ received: true });
});
```

|           |                                     |
| --------- | ----------------------------------- |
| Algorithm | HMAC-SHA256                         |
| Encoding  | Base64                              |
| Header    | `X-Webhook-Signature`               |
| Payload   | Raw JSON string of the request body |

{% hint style="warning" %}
Verify against the **raw request body bytes** when possible. Re-serializing `req.body` can reorder keys and break the signature. Store your secret securely (never in version control), use it only for verification, and rotate periodically.
{% endhint %}

## Delivery, retries & idempotency

* **Attempts:** each event is delivered up to **twice** — an initial attempt, then **one** retry about **30 seconds** later if the first doesn't succeed.
* **Per-attempt timeout: 10 seconds.** A slow response is treated as a failure.
* **Success = HTTP 2xx.** Any non-2xx (or a timeout) triggers the retry; after the second failure we stop, and the event is not redelivered later.
* **Idempotency:** a retry redelivers the same event, so **deduplicate on `eventId`** and make your handler idempotent. Return `200` quickly, then process asynchronously.

## Linking onramp ↔ wallet deposit

> Advanced — only relevant if you support fiat on-ramps that are swapped onward using smart wallets.

When a fiat on-ramp funds one of our smart wallets and we swap those funds onward, you receive **two independent deposits** — one `onramp` and one `smart_wallet` — each as its own `transaction.completed` event. They are correlated **on-chain**, by the tx that delivered crypto into the smart wallet:

* the **onramp** deposit carries that delivery tx as `depositTxHash`;
* the **smart\_wallet** deposit carries `fundingTxs` — every tx that funded the swap.

A smart\_wallet deposit belongs to an onramp deposit when its `fundingTxs` contains the onramp's `depositTxHash`:

```js
const isFundedBy = (smartWallet, onramp) =>
  smartWallet.fundingTxs?.some(
    (tx) => tx.toLowerCase() === onramp.depositTxHash?.toLowerCase(),
  );
```

Event order is **not guaranteed** — the swap often completes before the on-ramp settles — so buffer an unmatched deposit (keyed by the funding tx) until its partner arrives.

**Things to know**

* **Don't double-count volume.** The two deposits represent the same money (fiat → token). Count the flow once, keyed by the funding tx.
* **`onramp` completion depends on destination.** A standalone on-ramp (delivered straight to an external wallet) is fully done at `completed`. If it funds a smart wallet, final delivery is the linked `smart_wallet` deposit's event (which may arrive later).
* **`destinationAddress` differs by design:** the on-ramp deposit targets the smart wallet (intermediate); the smart\_wallet deposit targets the user's final wallet.
* **Field presence:** `fundingTxs` is only on `smart_wallet` deposits; `depositTxHash` is on `onramp` deposits and absent on plain `dex_swap`.
* **Compare hashes case-insensitively.**

## Support

If something looks wrong, open a ticket in [Discord](https://discord.gg/y8eevERxBz) with your `integratorId`, an example `eventId`, the relevant `deposit.id` and funding tx hash (`depositTxHash` or a `fundingTxs` entry), recent log entries, and your endpoint URL. See [Support](/resources/support).


# FAQ

## Integration

### Which package do I install?

**`@swapper-finance/deposit-sdk`** — the [iframe SDK](/widget-integration/iframe-sdk). It works in any framework, or none, and is the only package you need to embed Swapper.

### What's the minimum to get a widget on screen?

Four values — `integratorId`, `dstChainId`, `dstTokenAddr`, `depositWalletAddress` — passed to `openSwapperModal` or `new SwapperIframe`. See the [Quick Start](/getting-started/quick-start).

### How do I get an `integratorId`?

Open a ticket in [Discord](https://discord.gg/y8eevERxBz) — see [Support](/resources/support). The same registration links your [webhook](/tracking-deposits/webhooks) endpoint and secret.

### Can I change the destination or styling after the widget loads?

Yes. `updateConfig()`, `updateStyles()`, and `updateCustomContractCalls()` patch the live widget over `postMessage` — no reload. See the [iframe SDK](/widget-integration/iframe-sdk#methods).

### The first open is slow. Can I make it instant?

Preload it. Use [`preloadSwapperModal`](/widget-integration/modal-and-embed#preloading-the-modal) or [`preloadSwapperEmbed`](/widget-integration/modal-and-embed#inline-embed-swapperembed). The widget loads hidden in the background; smart-wallet authorization is deferred until the user actually opens it.

## Deposits

### What can users pay with?

A [card / bank on-ramp](/deposit-methods/fiat-onramp), a one-tap [smart-wallet deposit](/deposit-methods/transfer-crypto), or a [wallet transfer](/deposit-methods/wallet-deposit) from their own wallet on any supported chain. You choose which to offer with [`supportedDepositOptions`](/widget-integration/configuration#supporteddepositoptions).

### What do I actually receive?

Exactly your `dstTokenAddr` on `dstChainId`, delivered to `depositWalletAddress` — regardless of what the user paid with.

### Can I set deposit limits?

Yes — [`minDepositUsd` / `maxDepositUsd`](/widget-integration/configuration#mindepositusd--maxdepositusd), validated against the USD value of the received amount. The transfer flow also floors the minimum so gas is always covered.

### Can I run actions after the swap (stake, wrap, LP)?

Yes — [Custom Contract Calls](/widget-integration/custom-contract-calls) chain arbitrary on-chain actions after delivery, atomically.

## Tracking

### How do I know a deposit completed?

Two channels: a [widget event](/tracking-deposits/widget-events) in the browser (instant, for UX) and a [webhook](/tracking-deposits/webhooks) to your server (reliable, for accounting). Use both.

### Why did I get two webhooks for one deposit?

A card on-ramp that is swept onward through a smart wallet produces two linked deposits (`onramp` + `smart_wallet`). Count the flow **once**, keyed by the funding tx. See [Webhooks → Linking](/tracking-deposits/webhooks#linking-onramp--wallet-deposit).

### How do I verify a webhook is really from Swapper?

Check the `X-Webhook-Signature` header — HMAC-SHA256 (Base64) of the raw body with your secret. Code sample in [Webhooks](/tracking-deposits/webhooks#verifying-the-signature).

### Are webhooks retried?

Once — an initial attempt plus one retry \~30s later. Deduplicate on `eventId` and return `200` within 10 seconds. See [Delivery & idempotency](/tracking-deposits/webhooks#delivery-retries--idempotency).

## Amounts & custody

### What format are amounts in?

Base-unit strings (wei / lamports / smallest unit), never floats. Prices and USD values are decimal strings.

### Can Swapper move my users' funds?

No, by design. Signing and broadcasting always happen from the user's own wallet or smart wallet, never a Swapper-held key.


# Support & Links

## Getting started

* [**Quick Start**](/getting-started/quick-start) — a working embed in five minutes.
* [**Choosing an Integration**](/widget-integration/choosing-an-integration) — pick the right approach for your stack.

## Getting help

Support is on **Discord**. [Join the Swapper Discord](https://discord.gg/y8eevERxBz) and **open a ticket** to reach the team — for an `integratorId`, to set or change your webhook URL or signing secret, or for help with a specific deposit.

## Get an `integratorId`

Every integration is tied to a registered integrator id, which also links your [webhook](/tracking-deposits/webhooks) endpoint and signing secret. To request one — or to set/change your webhook URL or receive your secret — open a ticket in [Discord](https://discord.gg/y8eevERxBz).

## Contacting support

When you open a ticket about a specific deposit, include as much of the following as you have so we can find it fast:

* your **`integratorId`**;
* the webhook **`eventId`** (if applicable);
* the **`deposit.id`**;
* the relevant **tx hash** — `depositTxHash`, `txHash`, or a `fundingTxs` entry;
* for smart-wallet issues: the **`authorizationId`**, **`executionId`**, and **`smartWalletAddress`**;
* the **`error.id`** from any error you received;
* a rough **timestamp** and your endpoint URL.

## Packages

| Package                                                                                      | Purpose                                                                                                               |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [`@swapper-finance/deposit-sdk`](https://www.npmjs.com/package/@swapper-finance/deposit-sdk) | Iframe SDK — the integration package (iframe, modal, inline embed). See [Iframe SDK](/widget-integration/iframe-sdk). |

## Hosts

| Thing       | Host                               |
| ----------- | ---------------------------------- |
| Widget host | `https://deposit.swapper.finance/` |

## Reference

* [**Configuration Reference**](/widget-integration/configuration) — every widget option.
* [**Widget Events**](/tracking-deposits/widget-events) — browser-side notifications.
* [**Webhooks**](/tracking-deposits/webhooks) — server-side notifications.
* [**Glossary**](/getting-started/glossary) — every term in one place.


