> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfer Webhooks Setup

> Track wallet transfer status and receive real-time notifications when transfers complete

After initiating a token transfer, you can monitor its status and receive real-time notifications using webhooks. This guide shows you how to set up webhook listeners to track when transfers complete or fail.

## Prerequisites

* A Crossmint project with API keys configured
* A wallet with tokens to transfer (see [Get Staging Tokens](/wallets/guides/get-staging-tokens))
* The `svix` package installed for webhook signature verification:

<CodeGroup>
  ```bash npm theme={null}
  npm i svix
  ```

  ```bash yarn theme={null}
  yarn add svix
  ```

  ```bash pnpm theme={null}
  pnpm add svix
  ```

  ```bash bun theme={null}
  bun add svix
  ```
</CodeGroup>

## Choose your setup path

There are two ways to test and integrate webhooks:

* **Option 1: Test using Svix Play** - Quick testing without setting up a local server
* **Option 2: Integrate with your local app** - Full integration with your development environment

***

## Option 1: Test using Svix Play

Use Svix Play to inspect webhook payloads without setting up a local server. This is useful for understanding the event structure before building your integration.

### Step 1: Create a webhook endpoint with Svix Play

1. Navigate to the Webhooks page in the Crossmint Console
2. Click **Add Endpoint**
3. In the dialog that appears, click the **"with Svix Play"** button (instead of entering a custom URL)
4. Select the following event types:
   * `wallets.transfer.in` - Receive notifications when tokens arrive in your wallets
   * `wallets.transfer.out` - Receive notifications when tokens leave your wallets
5. Click **Create**

This automatically configures your endpoint to use Svix Play, which opens with a pre-filled URL like:

```
https://play.svix.com/in/e_0izvs3VwiAo8hvKf6Ygh2tujXre/
```

### Step 2: Trigger a transfer

From your app, call the `wallet.send()` method to create an outgoing transfer. Any transfer made with the same project API key will trigger a webhook event on this endpoint.

```typescript theme={null}
const RECIPIENT_ADDRESS = "0x..."; // String

const { hash, explorerLink } = await wallet.send(
    RECIPIENT_ADDRESS,
    "usdc",
    "1.00"
);
```

As soon as the transfer is created, a `wallets.transfer.out` event will appear in Svix Play, where you can inspect the full payload structure.

For the complete transfer setup, see the [Transfer Tokens](/wallets/guides/transfer-tokens) guide.

***

## Option 2: Integrate with your local app

For full integration, set up a local server to receive and process webhook events.

### Step 1: Expose your local server

Before registering a webhook endpoint, you need a publicly accessible URL. If your app runs locally on `http://localhost:3000`, use [ngrok](https://ngrok.com) to create a public URL:

```bash theme={null}
ngrok http 3000
```

This gives you a public URL like `https://abc123.ngrok.io` that forwards to your local server.

Alternatively, if you have a staging or development server with a public URL, you can register that directly.

### Step 2: Register your webhook endpoint

1. Navigate to the Webhooks page in the Crossmint Console
2. Click **Add Endpoint**
3. Enter your public URL with the webhook path (e.g., `https://abc123.ngrok.io/api/webhooks/transfers`)
4. Select the following event types:
   * `wallets.transfer.in` - Receive notifications when tokens arrive in your wallets
   * `wallets.transfer.out` - Receive notifications when tokens leave your wallets
5. Click **Create**
6. Copy the **signing secret** from the endpoint details - you will need this to verify webhook signatures

For detailed setup instructions, see [Add an Endpoint](/introduction/platform/webhooks/add-endpoint).

<Tip>
  Your webhook endpoint must return a 2xx HTTP status code within 15 seconds to acknowledge receipt. This is critical for webhook delivery confirmation.
</Tip>

### Step 3: Handle transfer events

Create an endpoint to receive and process webhook events. The following example shows how to handle both incoming and outgoing transfer events.

<Tabs>
  <Tab title="Node.js (Express)">
    ```javascript theme={null}
    import express from "express";
    import { Webhook } from "svix";

    const app = express();

    app.use("/api/webhooks", express.raw({ type: "application/json" }));

    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // String

    app.post("/api/webhooks/transfers", async (req, res) => {
        const wh = new Webhook(WEBHOOK_SECRET);
        let payload;

        try {
            payload = wh.verify(req.body, {
                "svix-id": req.headers["svix-id"],
                "svix-timestamp": req.headers["svix-timestamp"],
                "svix-signature": req.headers["svix-signature"],
            });
        } catch (err) {
            console.error("Webhook verification failed:", err.message);
            return res.status(400).json({ error: "Invalid signature" });
        }

        res.status(200).json({ received: true });

        await processTransferEvent(payload);
    });

    async function processTransferEvent(event) {
        const { type, data } = event;

        if (type === "wallets.transfer.in") {
            console.log("Incoming transfer received:");
            console.log(`  From: ${data.sender.address}`);
            console.log(`  To: ${data.recipient.address}`);
            console.log(`  Amount: ${data.token.amount} ${data.token.symbol}`);
            console.log(`  Explorer: ${data.onChain.explorerLink}`);
        }

        if (type === "wallets.transfer.out") {
            if (data.status === "succeeded") {
                console.log("Outgoing transfer succeeded:");
                console.log(`  To: ${data.recipient.address}`);
                console.log(`  Amount: ${data.token.amount} ${data.token.symbol}`);
                console.log(`  Explorer: ${data.onChain.explorerLink}`);
            } else {
                console.error("Outgoing transfer failed:");
                console.error(`  Error: ${data.error.message}`);
                console.error(`  Reason: ${data.error.reason}`);
            }
        }
    }

    app.listen(3000, () => console.log("Webhook server running on port 3000"));
    ```
  </Tab>

  <Tab title="Next.js (App Router)">
    ```typescript theme={null}
    // app/api/webhooks/transfers/route.ts
    import { Webhook } from "svix";
    import { headers } from "next/headers";
    import { NextResponse } from "next/server";

    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!; // String

    export async function POST(req: Request) {
        const body = await req.text();
        const headerPayload = await headers();

        const svixId = headerPayload.get("svix-id");
        const svixTimestamp = headerPayload.get("svix-timestamp");
        const svixSignature = headerPayload.get("svix-signature");

        if (!svixId || !svixTimestamp || !svixSignature) {
            return NextResponse.json({ error: "Missing headers" }, { status: 400 });
        }

        const wh = new Webhook(WEBHOOK_SECRET);
        let payload: any;

        try {
            payload = wh.verify(body, {
                "svix-id": svixId,
                "svix-timestamp": svixTimestamp,
                "svix-signature": svixSignature,
            });
        } catch (err) {
            console.error("Webhook verification failed:", err);
            return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
        }

        const { type, data } = payload;

        if (type === "wallets.transfer.in") {
            console.log(`Received ${data.token.amount} ${data.token.symbol}`);
            console.log(`From: ${data.sender.address}`);
            console.log(`Explorer: ${data.onChain.explorerLink}`);
        }

        if (type === "wallets.transfer.out") {
            if (data.status === "succeeded") {
                console.log(`Sent ${data.token.amount} ${data.token.symbol}`);
                console.log(`To: ${data.recipient.address}`);
            } else {
                console.error(`Transfer failed: ${data.error.message}`);
            }
        }

        return NextResponse.json({ received: true });
    }
    ```
  </Tab>

  <Tab title="Python (Flask)">
    ```python theme={null}
    import os
    from flask import Flask, request, jsonify
    from svix.webhooks import Webhook

    app = Flask(__name__)

    WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]  # String

    @app.route("/api/webhooks/transfers", methods=["POST"])
    def handle_transfer_webhook():
        headers = {
            "svix-id": request.headers.get("svix-id"),
            "svix-timestamp": request.headers.get("svix-timestamp"),
            "svix-signature": request.headers.get("svix-signature"),
        }

        wh = Webhook(WEBHOOK_SECRET)
        try:
            payload = wh.verify(request.get_data(as_text=True), headers)
        except Exception as e:
            print(f"Webhook verification failed: {e}")
            return jsonify({"error": "Invalid signature"}), 400

        event_type = payload.get("type")
        data = payload.get("data")

        if event_type == "wallets.transfer.in":
            print(f"Received {data['token']['amount']} {data['token']['symbol']}")
            print(f"From: {data['sender']['address']}")
            print(f"Explorer: {data['onChain']['explorerLink']}")

        if event_type == "wallets.transfer.out":
            if data["status"] == "succeeded":
                print(f"Sent {data['token']['amount']} {data['token']['symbol']}")
                print(f"To: {data['recipient']['address']}")
            else:
                print(f"Transfer failed: {data['error']['message']}")

        return jsonify({"received": True})

    if __name__ == "__main__":
        app.run(port=3000)
    ```
  </Tab>
</Tabs>

<Warning>
  Always use the **raw request body** when verifying webhooks. Parsing and re-stringifying JSON will alter the content and cause verification failure.
</Warning>

<Note>
  For more details on securing and verifying webhook requests, see [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks).
</Note>

### Step 4: Test your endpoint

Execute a transfer using the [Transfer Tokens](/wallets/guides/transfer-tokens) guide and observe the webhook event in your server logs. For a successful outgoing transfer, you should see output like:

```
Sent 0.40 USDC
To: 0xdf8b5f9c19e187f1ea00730a1e46180152244315
```

## Best practices

* **Respond quickly**: Return a 2xx status within 15 seconds to prevent retries
* **Verify signatures**: Always verify webhook signatures before processing. For comprehensive guidance, see [Webhook Best Practices](/introduction/platform/webhooks/best-practices)

## Related guides

* [Transfer Webhooks Reference](/wallets/guides/webhooks) - Complete event schema and field reference
* [Transfer Tokens](/wallets/guides/transfer-tokens) - Initiate token transfers
* [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) - Signature verification details
* [Add an Endpoint](/introduction/platform/webhooks/add-endpoint) - Configure webhook endpoints
