Webhooks — Node.js SDK

Verify and handle webhooks

The SDK provides a built-in method to verify webhook signatures and parse events:
import express from 'express';
import { Simiz } from '@simiz/node-sdk';

const app = express();
const simiz = new Simiz(process.env.SIMIZ_SECRET_KEY);

app.post('/webhooks/simiz',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-simiz-signature'] as string;

    try {
      const event = simiz.webhooks.constructEvent(
        req.body,
        signature,
        process.env.SIMIZ_WEBHOOK_SECRET
      );

      switch (event.type) {
        case 'payment.succeeded':
          const payment = event.data;
          console.log('Payment received:', payment.id);
          // Update order status in your database
          break;

        case 'payment.failed':
          console.log('Payment failed:', event.data.id);
          // Notify customer, offer retry
          break;

        case 'refund.succeeded':
          console.log('Refund completed:', event.data.id);
          // Update order and notify customer
          break;

        case 'subscription.renewed':
          console.log('Subscription renewed:', event.data.id);
          // Extend subscription access
          break;

        default:
          console.log('Unhandled event type:', event.type);
      }

      res.status(200).json({ received: true });
    } catch (err) {
      console.error('Webhook error:', err.message);
      res.status(400).send(`Webhook Error: ${err.message}`);
    }
  }
);
Important: Use express.raw() instead of express.json() for the webhook route. The signature verification requires the raw request body.

Manual signature verification

If you prefer to verify signatures manually:
Signature format: X-Simiz-Signature: t=<timestamp>,v1=<hmac_sha256_hex>The timestamp protects against replay attacks. Signatures older than 5 minutes are rejected.
import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds: number = 300
): boolean {
  // Parse signature header: t=<timestamp>,v1=<hash>
  const parts = signatureHeader.split(',');
  let timestamp: number | undefined;
  let hash: string | undefined;

  for (const part of parts) {
    if (part.startsWith('t=')) {
      timestamp = parseInt(part.slice(2), 10);
    } else if (part.startsWith('v1=')) {
      hash = part.slice(3);
    }
  }

  if (timestamp === undefined || !hash) {
    return false;
  }

  // Check timestamp is within tolerance (default: 5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) {
    return false;
  }

  // Calculate expected signature
  const signaturePayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signaturePayload)
    .digest('hex');

  // Timing-safe comparison
  try {
    return crypto.timingSafeEqual(
      Buffer.from(hash, 'hex'),
      Buffer.from(expectedSignature, 'hex')
    );
  } catch {
    return false;
  }
}

Best practices

  • Respond with 200 immediately — Process events asynchronously with a job queue
  • Handle duplicates — Use the event id to check if you’ve already processed this event
  • Log raw payloads — Store the raw JSON for debugging failed webhook processing
  • Use a dedicated route — Don’t mix webhook handling with other API routes