> ## 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.

# Wallet Webhooks

> Set up webhooks to receive real-time events including transfers and signer exports

Wallet webhooks allow you to receive real-time notifications for events that occur on wallets in your project. This enables you to track transfers, monitor signer exports, update your application state, and trigger automated workflows.

Crossmint currently supports the following wallet webhook event types:

| Event                     | Description                                            |
| ------------------------- | ------------------------------------------------------ |
| `wallets.transfer.in`     | Tokens transferred **into** a project wallet           |
| `wallets.transfer.out`    | Tokens transferred **out of** a project wallet         |
| `wallets.signer.exported` | A wallet's private key was exported for the first time |

## Transfer Webhooks

Transfer webhooks notify you when tokens are transferred in or out of wallets in your project.

## Supported Configurations

Transfer webhooks have different levels of support depending on the blockchain and wallet type:

| Chain                                   | Wallet Types           | Token Types                                                      |
| --------------------------------------- | ---------------------- | ---------------------------------------------------------------- |
| **EVM** (Ethereum, Polygon, Base, etc.) | Smart wallets and EOAs | ERC-20 tokens only (native tokens like ETH, MATIC not supported) |
| **Solana**                              | Smart wallets and EOAs | All SPL tokens including native SOL                              |
| **Stellar**                             | Smart wallets and EOAs | Soroban tokens (fungible)                                        |

## Event Types

Crossmint provides two webhook event types for wallet transfers:

### `wallets.transfer.in`

Triggered when tokens are transferred **into** a wallet in your project. This event is only sent for successful transfers that have been confirmed onchain.

### `wallets.transfer.out`

Triggered when tokens are transferred **out of** a wallet in your project. This event is sent for both successful and failed transfer attempts, allowing you to track the complete lifecycle of outgoing transactions.

## Setup

To start receiving wallet transfer webhooks, configure a webhook endpoint in the Crossmint Console.

### Prerequisites

* A Crossmint project with API keys configured
* A wallet with tokens to transfer (see [Fund a Staging Wallet](/wallets/guides/fund-staging-wallet))
* 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>

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.

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/
```

Trigger a transfer from your app by calling `wallet.send()`, and the event will appear in Svix Play:

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

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

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

### Option 2: Local development with ngrok

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

Before registering a webhook endpoint, you need a publicly accessible URL. If your app runs locally on `http://localhost:3000`, use <a href="https://ngrok.com" target="_blank">ngrok</a> 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.

### Register your 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>

### Handle and verify 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;

    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 });

        processTransferEvent(payload).catch(err => console.error("Event processing failed:", err));
    });

    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!;

    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"]

    @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>

### 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
```

## Event Schema

Both `wallets.transfer.in` and `wallets.transfer.out` events share a common structure with some key differences based on the transfer direction and outcome.

### Common Fields

All wallet transfer events include the following fields:

| Field                        | Type              | Description                                                                                                                   |
| ---------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `id`                         | string            | Unique identifier for the webhook event                                                                                       |
| `type`                       | string            | Event type: `"wallets.transfer.in"` or `"wallets.transfer.out"`                                                               |
| `data.sender`                | object            | Information about the sender wallet                                                                                           |
| `data.sender.address`        | string            | Blockchain address of the sender                                                                                              |
| `data.sender.chain`          | string            | Chain identifier (e.g., `"ethereum"`, `"polygon"`, `"solana"`, `"stellar"`)                                                   |
| `data.sender.locator`        | string            | Wallet locator in the format `chain:address`                                                                                  |
| `data.sender.owner`          | string (optional) | Owner identifier if the sender is a Crossmint-managed wallet                                                                  |
| `data.recipient`             | object            | Information about the recipient wallet                                                                                        |
| `data.recipient.address`     | string            | Blockchain address of the recipient                                                                                           |
| `data.recipient.chain`       | string            | Chain identifier                                                                                                              |
| `data.recipient.locator`     | string            | Wallet locator in the format `chain:address`                                                                                  |
| `data.recipient.owner`       | string (optional) | Owner identifier for the recipient wallet                                                                                     |
| `data.token`                 | object            | Information about the transferred token                                                                                       |
| `data.token.type`            | string            | Token type, currently only `"fungible"` is supported                                                                          |
| `data.token.chain`           | string            | Chain where the token exists                                                                                                  |
| `data.token.locator`         | string            | Token locator in the format `chain:contractAddress`                                                                           |
| `data.token.amount`          | string            | Human-readable amount (adjusted for decimals)                                                                                 |
| `data.token.rawAmount`       | string            | Raw amount in smallest unit (e.g., wei for ETH)                                                                               |
| `data.token.contractAddress` | string (EVM)      | Token contract address for EVM chains                                                                                         |
| `data.token.mintHash`        | string (Solana)   | Token mint address for Solana                                                                                                 |
| `data.token.contractId`      | string (Stellar)  | Token contract ID for Stellar                                                                                                 |
| `data.token.decimals`        | number            | Number of decimals for the token                                                                                              |
| `data.token.symbol`          | string (optional) | Token symbol (e.g., `"USDC"`, `"ETH"`)                                                                                        |
| `data.status`                | string            | Transfer status: `"succeeded"` or `"failed"`                                                                                  |
| `data.transferId`            | string (optional) | Unique identifier for the transfer, can be used with the [`Get Transaction API`](/api-reference/wallets/get-transaction)      |
| `data.completedAt`           | string            | ISO 8601 timestamp when the transfer was completed. Timestamp may be off by a few seconds from the actual onchain transaction |

### Type-Specific Differences

**Incoming Transfers (`wallets.transfer.in`)**:

* `data.status` is always `"succeeded"` (only successful transfers are reported)
* `data.onChain` is always present with transaction details

**Outgoing Transfers (`wallets.transfer.out`)**:

* `data.status` can be `"succeeded"` or `"failed"`
* `data.onChain` is present only when `status` is `"succeeded"`
* `data.error` is present only when `status` is `"failed"`

<Note>
  The `transferId` field is only available for transfers initiated through the Crossmint API. It can be used to retrieve the transfer details from the [`Get Transaction API`](/api-reference/wallets/get-transaction).
</Note>

### Event Examples

<Accordion title="Incoming Transfer Example">
  ```json theme={null}
  {
    "id": "whevnt_12324",
    "type": "wallets.transfer.in",
    "data": {
      "transferId": "660e8400-e29b-41d4-a716-446655440002",
      "sender": {
        "address": "0x1234567890123456789012345678901234567890",
        "chain": "ethereum",
        "locator": "ethereum:0x1234567890123456789012345678901234567890"
      },
      "recipient": {
        "address": "0x0987654321098765432109876543210987654321",
        "chain": "ethereum",
        "locator": "ethereum:0x0987654321098765432109876543210987654321",
        "owner": "email:user@example.com"
      },
      "token": {
        "type": "fungible",
        "chain": "ethereum",
        "locator": "ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "amount": "100.50",
        "rawAmount": "100500000",
        "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "decimals": 6,
        "symbol": "USDC"
      },
      "status": "succeeded",
      "completedAt": "2025-10-21T12:34:56.789Z",
      "onChain": {
        "txId": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        "explorerLink": "https://etherscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
      }
    }
  }
  ```
</Accordion>

<Accordion title="Incoming Transfer (Stellar) Example">
  ```json theme={null}
  {
    "id": "whevnt_34567",
    "type": "wallets.transfer.in",
    "data": {
      "transferId": "770e8400-e29b-41d4-a716-446655440003",
      "sender": {
        "address": "GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOUJ3DQMFX7AQZED",
        "chain": "stellar",
        "locator": "stellar:GDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOUJ3DQMFX7AQZED"
      },
      "recipient": {
        "address": "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI",
        "chain": "stellar",
        "locator": "stellar:GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI",
        "owner": "email:user@example.com"
      },
      "token": {
        "type": "fungible",
        "chain": "stellar",
        "locator": "stellar:CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
        "amount": "100.5000000",
        "rawAmount": "1005000000",
        "contractId": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
        "decimals": 7,
        "symbol": "USDC"
      },
      "status": "succeeded",
      "completedAt": "2025-10-21T12:34:56.789Z",
      "onChain": {
        "txId": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
        "explorerLink": "https://stellar.expert/explorer/public/tx/abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
      }
    }
  }
  ```
</Accordion>

<Accordion title="Outgoing Transfer (Success) Example">
  ```json theme={null}
  {
    "id": "whevnt_56789",
    "type": "wallets.transfer.out",
    "data": {
      "transferId": "550e8400-e29b-41d4-a716-446655440000",
      "sender": {
        "address": "0x0987654321098765432109876543210987654321",
        "chain": "ethereum",
        "locator": "ethereum:0x0987654321098765432109876543210987654321",
        "owner": "email:user@example.com"
      },
      "recipient": {
        "address": "0x1234567890123456789012345678901234567890",
        "chain": "ethereum",
        "locator": "ethereum:0x1234567890123456789012345678901234567890"
      },
      "token": {
        "type": "fungible",
        "chain": "ethereum",
        "locator": "ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "amount": "50.25",
        "rawAmount": "50250000",
        "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "decimals": 6,
        "symbol": "USDC"
      },
      "status": "succeeded",
      "completedAt": "2025-10-21T12:34:56.789Z",
      "onChain": {
        "txId": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
        "explorerLink": "https://etherscan.io/tx/0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
      }
    }
  }
  ```
</Accordion>

<Accordion title="Outgoing Transfer (Failed) Example">
  ```json theme={null}
  {
    "id": "whevnt_99999",
    "type": "wallets.transfer.out",
    "data": {
      "transferId": "550e8400-e29b-41d4-a716-446655440001",
      "sender": {
        "address": "0x0987654321098765432109876543210987654321",
        "chain": "ethereum",
        "locator": "ethereum:0x0987654321098765432109876543210987654321",
        "owner": "email:user@example.com"
      },
      "recipient": {
        "address": "0x1234567890123456789012345678901234567890",
        "chain": "ethereum",
        "locator": "ethereum:0x1234567890123456789012345678901234567890"
      },
      "token": {
        "type": "fungible",
        "chain": "ethereum",
        "locator": "ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "amount": "1000.00",
        "rawAmount": "1000000000",
        "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "decimals": 6,
        "symbol": "USDC"
      },
      "status": "failed",
      "completedAt": "2025-10-21T12:34:56.789Z",
      "error": {
        "message": "Transaction reverted",
        "reason": "execution_reverted",
        "revert": {
          "type": "contract_call",
          "reason": "ERC20: transfer amount exceeds balance"
        }
      }
    }
  }
  ```
</Accordion>

### Error Fields (Outgoing Transfers Only)

When an outgoing transfer fails, the following error fields are included:

| Field                      | Type           | Description                                                                        |
| -------------------------- | -------------- | ---------------------------------------------------------------------------------- |
| `data.error.message`       | string         | Error message                                                                      |
| `data.error.reason`        | string         | Error reason (e.g., `"execution_reverted"`, `"program_error"`, `"build_failed"`)   |
| `data.error.revert`        | object         | Revert details from smart contract                                                 |
| `data.error.revert.type`   | string         | Revert type: `"contract_call"`, `"wallet_authorization"`, or `"wallet_deployment"` |
| `data.error.revert.reason` | string         | Revert reason message                                                              |
| `data.error.logs`          | any (optional) | Additional error logs                                                              |

## Best Practices

For comprehensive guidance on implementing reliable webhook handlers, see [Webhook Best Practices](/introduction/platform/webhooks/best-practices).

## Signer Export Webhook

The `wallets.signer.exported` event is triggered when a user exports the private key of a wallet for the **first time**. This webhook is sent once per wallet — subsequent exports of the same wallet do not trigger additional events.

This is useful for compliance monitoring, alerting, or updating internal records when a user gains direct access to the underlying key material.

<Note>
  The webhook payload does not contain any private key material. It only identifies which wallet and signer were involved in the export.
</Note>

### Event Schema

| Field                | Type   | Description                                                           |
| -------------------- | ------ | --------------------------------------------------------------------- |
| `id`                 | string | Unique identifier for the webhook event                               |
| `type`               | string | `"wallets.signer.exported"`                                           |
| `data.owner`         | string | Signer locator of the wallet owner (e.g., `"email:user@example.com"`) |
| `data.walletAddress` | string | Blockchain address of the exported wallet                             |
| `data.chain`         | string | Chain type: `"evm"`, `"solana"`, or `"stellar"`                       |
| `data.signerType`    | string | Signer type that was exported: `"email"` or `"phone"`                 |

### Event Example

<Accordion title="Signer Export Example">
  ```json theme={null}
  {
    "id": "evt_abc123",
    "type": "wallets.signer.exported",
    "data": {
      "owner": "email:user@example.com",
      "walletAddress": "0x0987654321098765432109876543210987654321",
      "chain": "evm",
      "signerType": "email"
    }
  }
  ```
</Accordion>

### Setup

To start receiving signer export webhooks, add a webhook endpoint in the Crossmint Console and select the `wallets.signer.exported` event type. For detailed setup instructions, see [Add an Endpoint](/introduction/platform/webhooks/add-endpoint).

* **Respond quickly**: Return a 2xx status within 15 seconds to prevent retries
* **Verify signatures**: Always verify webhook signatures before processing

For comprehensive guidance on implementing reliable webhook handlers, see [Webhook Best Practices](/introduction/platform/webhooks/best-practices).

## Related Resources

* [Add an Endpoint](/introduction/platform/webhooks/add-endpoint) - Configure webhook endpoints
* [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) - Verify webhook signatures
* [Best Practices](/introduction/platform/webhooks/best-practices) - Webhook implementation best practices
* [Create Wallet](/wallets/guides/create-wallet) - Create wallets to receive transfers
* [Transfer Tokens](/wallets/guides/transfer-tokens) - Initiate token transfers
* [Export Private Key](/wallets/guides/export-private-key) - Allow users to export private keys
