Skip to main content
Docs/Node.js SDK/Webhooks
SDK Reference

Webhook verification

The SDK provides client.webhooks.constructEvent() to verify webhook signatures and parse the payload. It throws StaffifyError if the signature is invalid or the timestamp is more than 5 minutes old.

Signature format

Staffify signs every webhook request with HMAC-SHA256. The signature is sent in the X-Staffify-Signature header as sha256=<hex>. The timestamp is in X-Staffify-Timestamp (Unix seconds). The signed content is timestamp.body.

Express example

TypeScript

import express from 'express';
import Staffify, { StaffifyError } from 'staffify';

const app = express();
const client = new Staffify({ apiKey: process.env.STAFFIFY_API_KEY });

// Use raw body — must come before express.json()
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const payload = client.webhooks.constructEvent(
      req.body,                                       // Buffer
      req.headers['x-staffify-signature'] as string,
      req.headers['x-staffify-timestamp'] as string,
      process.env.WEBHOOK_SECRET!
    );

    switch (payload.event) {
      case 'call.ended':
        console.log('Call ended:', payload.data);
        break;
      case 'call.recording_ready':
        console.log('Recording ready:', payload.data);
        break;
    }

    res.json({ received: true });
  } catch (err) {
    if (err instanceof StaffifyError) {
      console.error('Webhook verification failed:', err.message);
      return res.status(400).json({ error: err.message });
    }
    throw err;
  }
});

Fastify example

TypeScript

import Fastify from 'fastify';
import Staffify, { StaffifyError } from 'staffify';

const app = Fastify();
const client = new Staffify({ apiKey: process.env.STAFFIFY_API_KEY });

app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (_req, body, done) => {
  done(null, body);
});

app.post('/webhooks', async (req, reply) => {
  try {
    const payload = client.webhooks.constructEvent(
      req.body as Buffer,
      req.headers['x-staffify-signature'] as string,
      req.headers['x-staffify-timestamp'] as string,
      process.env.WEBHOOK_SECRET!
    );

    console.log('Event:', payload.event, payload.data);
    return { received: true };
  } catch (err) {
    if (err instanceof StaffifyError) {
      return reply.status(400).send({ error: err.message });
    }
    throw err;
  }
});

Important

  • Always use the raw request body — not the parsed JSON. Parsing and re-serializing changes whitespace and key order, which breaks the signature.
  • Never skip signature verification — always call constructEvent(), even in development.
  • The 5-minute timestamp window protects against replay attacks.
  • Rotate webhook secrets from client.webhooks.rotate(webhookId) or for agent webhooks with client.agents.rotateWebhookSecret(agentId).

Getting your webhook secret

Your secret is shown once when you create a webhook or rotate it. For agent webhooks, use client.agents.rotateWebhookSecret(). For org webhooks, use client.webhooks.rotate().

Webhook Verification - Node.js SDK - Staffify