> For the complete documentation index, see [llms.txt](https://docs.swapper.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.swapper.finance/tracking-deposits/webhooks.md).

# 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.md), 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.md).

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.md).
