How to configure webhooks for real-time event notifications.

Setup

What Are Webhooks?

A webhook is a URL on your server that Passkallet calls when something happens. Instead of polling, Passkallet tells you immediately.

If your server does not answer with a 2xx, Passkallet keeps trying: after 5s, 30s, 2m, 10m, 1h and 6h. Seven attempts over about eight hours, and the schedule is stored, so it survives a restart on either side.

Every request carries X-Passkallet-Delivery (the same id across all attempts of one event) and X-Passkallet-Attempt. Use the delivery id to make your handler idempotent: a retry is the SAME event, not a new one.

A 4xx is retried too — a route answering 404 while it is being deployed is exactly what this is for. The one exception is 410 Gone, which we take to mean stop sending. After the last attempt the delivery is kept as failed rather than discarded, and support can replay it.

Configure a Webhook

Webhooks are configured per organization through the dashboard or via the organization management API (not through the BaaS API).

When you set a webhook URL:

  • The URL must use HTTPS
  • A webhook secret is auto-generated and returned once -- save it immediately
  • The secret is used for HMAC-SHA256 signature verification
  • All transaction events for wallets in the organization are sent to this URL
The webhook secret is shown only once when you configure the webhook. If you lose it, you will need to reconfigure the webhook to get a new secret.

Test Your Webhook

You can test your webhook endpoint through the organization dashboard. This sends a test payload to verify your URL is reachable:

json
{
  "event": "webhook.test",
  "timestamp": "2026-05-31T12:00:00.000Z",
  "data": {
    "transactionUuid": "00000000-0000-0000-0000-000000000000",
    "transactionHash": null,
    "transactionType": "TEST",
    "transactionStatus": "TEST",
    "walletAddress": "0x0000000000000000000000000000000000000000",
    "externalUserId": null,
    "externalUserLabel": null,
    "details": {}
  },
  "signature": "sha256=abc123..."
}

Building Your Webhook Endpoint

javascript
const express = require("express");
const crypto = require("crypto");

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.PASSKALLET_WEBHOOK_SECRET;

app.post("/webhooks/passkallet", (req, res) => {
  const signatureHeader = req.headers["x-passkallet-signature"];

  // The signature is computed over the payload without the signature field
  const { signature: bodySig, ...payloadWithoutSig } = req.body;
  const payloadString = JSON.stringify(payloadWithoutSig);

  const expectedSignature =
    "sha256=" +
    crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(payloadString)
      .digest("hex");

  if (signatureHeader !== expectedSignature) {
    return res.status(401).send("Invalid signature");
  }

  const { event, data } = req.body;

  switch (event) {
    case "transaction.confirmed":
      console.log("Transaction confirmed:", data.transactionUuid);
      break;
    case "transaction.approved":
      console.log("Transaction approved:", data.transactionUuid);
      break;
    case "transaction.failed":
      console.log("Transaction failed:", data.transactionUuid);
      break;
    case "transaction.rejected":
      console.log("Transaction rejected:", data.transactionUuid);
      break;
    case "webhook.test":
      console.log("Webhook test received");
      break;
  }

  res.status(200).send("OK");
});
Respond quickly. Return 200 as fast as possible. If you need heavy processing, queue the event and process it asynchronously.