Skip to main content
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)
  • The svix package installed for webhook signature verification:
npm i svix

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.
const { hash, explorerLink } = await wallet.send(
  "<RECIPIENT_ADDRESS>",
  "solana: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 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 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.

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.
Your webhook endpoint must return a 2xx HTTP status code within 15 seconds to acknowledge receipt. This is critical for webhook delivery confirmation.

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.
Replace <YOUR_WEBHOOK_SECRET> with the signing secret from Step 2.
import express from "express";
import { Webhook } from "svix";

const app = express();

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

const WEBHOOK_SECRET = <YOUR_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"));
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.

Step 4: 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

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