Skip to main content

Guidelines

To implement webhooks in your application, follow these essential steps:
1

Configure Your Endpoint

Create a publicly accessible HTTP endpoint in your application that can receive POST requests. This endpoint must be available over HTTPS and return a 2xx status code to acknowledge receipt of webhook events.
Your webhook endpoint must be publicly accessible. For local development, use tools like ngrok to expose your local server.
2

Configure Webhooks

Configure your webhook endpoint URL directly through the Partners Dashboard or contact your Gnosis Pay partner manager for assistance. Provide the complete HTTPS URL where you want to receive webhook notifications.
Partners Dashboard: For partners registered through the dashboard, webhook configuration is available instantly in your dashboard settings. Manual Setup: If you need technical support assistance, webhook configuration typically takes 1-2 business days.
3

Receive and Verify Events

When events happen in the Gnosis Pay system, we’ll send HTTP POST requests to your webhook endpoint with event data and cryptographic signatures.All webhooks include cryptographic signatures using Ed25519 asymmetric cryptography:
  • X-Webhook-Timestamp: Unix timestamp when the webhook was sent
  • X-Webhook-Signature: Base64-encoded Ed25519 signature
Always verify webhook signatures before processing events. This ensures the webhook originated from Gnosis Pay and hasn’t been tampered with.
Retrieve the public key for signature verification from our API:
cURL
curl -X GET https://webhooks.gnosispay.com/api/v1/public-key
4

Parse and Process Event Data

Extract the eventType and data fields from the webhook payload. The eventType identifies what happened (e.g., user.created, kyc.status.changed), while data contains the complete entity information.
{
  "eventType": "user.created",
  "data": {
    "id": "user_123",
    "email": "user@example.com",
    // ... complete user entity
  }
}
Handle each event type appropriately in your application. Since we send complete entity data, you typically won’t need additional API calls to get the full context.
Process events idempotently to handle potential duplicates, and implement proper error handling and logging for monitoring.
Retry Policy: If your webhook endpoint returns a non-2xx status code, we’ll retry delivery up to 3 times with exponential backoff (1 minute, 5 minutes, 15 minutes).
Timeout: Your webhook endpoint must respond within 30 seconds. Requests that exceed this timeout are considered failed and will trigger our retry mechanism.

Complete Example

import express from "express";
import crypto from "crypto";

const app = express();

// Use raw body parsing for webhook signature verification
app.use("/webhook", express.raw({ type: "application/json" }));
app.use(express.json());

app.post("/webhook", async (req: express.Request, res: express.Response) => {
  try {
    // Verify the webhook signature
    const isValid = await verifyWebhookSignature(req);
    if (!isValid) {
      return res.status(401).send("Invalid signature");
    }

    // Parse the body after verification
    const payload = JSON.parse(req.body.toString());
    const { eventType, data } = payload;
    await processWebhookEvent(eventType, data);

    // Acknowledge receipt
    res.status(200).send("OK");
  } catch (error) {
    console.error("Webhook processing error:", error);
    res.status(500).send("Internal server error");
  }
});

async function verifyWebhookSignature(req: express.Request): Promise<boolean> {
  try {
    const signature = req.headers["x-webhook-signature"] as string;
    const timestamp = req.headers["x-webhook-timestamp"] as string;

    // Check if required headers are present
    if (!timestamp || !signature) {
      console.error(
        "Missing required headers: x-webhook-timestamp or x-webhook-signature"
      );
      return false;
    }

    // Use the raw body for signature verification
    const body = req.body.toString();

    // Retrieve the public key from Gnosis Pay
    const publicKeyResponse = await fetch(
      "https://webhooks.gnosispay.com/api/v1/public-key"
    );
    const keyData = await publicKeyResponse.json();

    if (!keyData.success || !keyData.publicKey) {
      throw new Error("Failed to fetch public key");
    }

    const { publicKey } = keyData;

    // Create the payload for verification (same as Bun implementation)
    const signingPayload = `${timestamp}.${body}`;

    // Verify the signature using the same method as Bun
    return crypto.verify(
      null,
      Buffer.from(signingPayload, "utf8"),
      publicKey,
      Buffer.from(signature, "base64")
    );
  } catch (error) {
    console.error("Signature verification failed:", error);
    return false;
  }
}

async function processWebhookEvent(eventType: string, data: any) {
  switch (eventType) {
    case "user.created":
      console.log("user.created", data);
      break;
    case "kyc.status.changed":
      console.log("kyc.status.changed", data);
      break;
    case "card.transaction.created":
      console.log("card.transaction.created", data);
      break;
    // Handle other event types
    default:
      console.log(`Unhandled event type: ${eventType}`);
  }
}