Skip to main content
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:
EventDescription
wallets.transfer.inTokens transferred into a project wallet
wallets.transfer.outTokens transferred out of a project wallet
wallets.signer.exportedA 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:
ChainWallet TypesToken Types
EVM (Ethereum, Polygon, Base, etc.)Smart wallets and EOAsERC-20 tokens only (native tokens like ETH, MATIC not supported)
SolanaSmart wallets and EOAsAll SPL tokens including native SOL
StellarSmart wallets and EOAsSoroban 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)
  • The svix package installed for webhook signature verification:
npm i svix
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:
const RECIPIENT_ADDRESS = "0x...";

const { hash, explorerLink } = await wallet.send(
    RECIPIENT_ADDRESS,
    "usdc",
    "1.00"
);
For the complete transfer setup, see the 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 ngrok to create a public URL:
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.
Your webhook endpoint must return a 2xx HTTP status code within 15 seconds to acknowledge receipt. This is critical for webhook delivery confirmation.

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.
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"));
Always use the raw request body when verifying webhooks. Parsing and re-stringifying JSON will alter the content and cause verification failure.
For more details on securing and verifying webhook requests, see Verify Webhooks.

Test your endpoint

Execute a transfer using the 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:
FieldTypeDescription
idstringUnique identifier for the webhook event
typestringEvent type: "wallets.transfer.in" or "wallets.transfer.out"
data.senderobjectInformation about the sender wallet
data.sender.addressstringBlockchain address of the sender
data.sender.chainstringChain identifier (e.g., "ethereum", "polygon", "solana", "stellar")
data.sender.locatorstringWallet locator in the format chain:address
data.sender.ownerstring (optional)Owner identifier if the sender is a Crossmint-managed wallet
data.recipientobjectInformation about the recipient wallet
data.recipient.addressstringBlockchain address of the recipient
data.recipient.chainstringChain identifier
data.recipient.locatorstringWallet locator in the format chain:address
data.recipient.ownerstring (optional)Owner identifier for the recipient wallet
data.tokenobjectInformation about the transferred token
data.token.typestringToken type, currently only "fungible" is supported
data.token.chainstringChain where the token exists
data.token.locatorstringToken locator in the format chain:contractAddress
data.token.amountstringHuman-readable amount (adjusted for decimals)
data.token.rawAmountstringRaw amount in smallest unit (e.g., wei for ETH)
data.token.contractAddressstring (EVM)Token contract address for EVM chains
data.token.mintHashstring (Solana)Token mint address for Solana
data.token.contractIdstring (Stellar)Token contract ID for Stellar
data.token.decimalsnumberNumber of decimals for the token
data.token.symbolstring (optional)Token symbol (e.g., "USDC", "ETH")
data.statusstringTransfer status: "succeeded" or "failed"
data.transferIdstring (optional)Unique identifier for the transfer, can be used with the Get Transaction API
data.completedAtstringISO 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"
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.

Event Examples

{
  "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"
    }
  }
}
{
  "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"
    }
  }
}
{
  "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"
    }
  }
}
{
  "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"
      }
    }
  }
}

Error Fields (Outgoing Transfers Only)

When an outgoing transfer fails, the following error fields are included:
FieldTypeDescription
data.error.messagestringError message
data.error.reasonstringError reason (e.g., "execution_reverted", "program_error", "build_failed")
data.error.revertobjectRevert details from smart contract
data.error.revert.typestringRevert type: "contract_call", "wallet_authorization", or "wallet_deployment"
data.error.revert.reasonstringRevert reason message
data.error.logsany (optional)Additional error logs

Best Practices

For comprehensive guidance on implementing reliable webhook handlers, see Webhook 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.
The webhook payload does not contain any private key material. It only identifies which wallet and signer were involved in the export.

Event Schema

FieldTypeDescription
idstringUnique identifier for the webhook event
typestring"wallets.signer.exported"
data.ownerstringSigner locator of the wallet owner (e.g., "email:user@example.com")
data.walletAddressstringBlockchain address of the exported wallet
data.chainstringChain type: "evm", "solana", or "stellar"
data.signerTypestringSigner type that was exported: "email" or "phone"

Event Example

{
  "id": "evt_abc123",
  "type": "wallets.signer.exported",
  "data": {
    "owner": "email:user@example.com",
    "walletAddress": "0x0987654321098765432109876543210987654321",
    "chain": "evm",
    "signerType": "email"
  }
}

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