Skip to main content
POST
events

Secure your endpoint

Verify that all webhook requests are generated by Kaizen by checking the signature in each request’s X-Webhooks-Signature header.

Verify the signature

Each webhook request includes these headers:
  • X-Webhooks-Signature: HMAC signature (includes version prefix)
  • X-Webhooks-Timestamp: Unix timestamp in seconds
  • X-Webhooks-Id: Unique identifier for this webhook delivery
To verify a signature:
  1. Extract the signature from X-Webhooks-Signature (remove the version prefix)
  2. Get the timestamp from X-Webhooks-Timestamp
  3. Get the webhook ID from X-Webhooks-Id
  4. Read the raw request body as a string (before JSON parsing)
  5. Construct the signed payload: {webhookId}.{timestamp}.{raw_body}
  6. Compute HMAC-SHA256 of the signed payload using your decoded secret
  7. Compare the computed signature with the received signature using constant-time comparison

Code Examples

const crypto = require('crypto');

function verifyWebhookSignature(
  webhookId,
  payload,
  signature,
  timestamp,
  secret
) {
  const signedPayload = `${webhookId}.${timestamp}.${payload}`;
  const keyBytes = Buffer.from(secret, 'base64url');
  const expectedSignature = crypto
    .createHmac('sha256', keyBytes)
    .update(signedPayload, 'utf8')
    .digest('hex');

  if (signature.length !== expectedSignature.length) {
    return false;
  }

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const webhookId = req.headers['x-webhooks-id'];
  const signatureHeader = req.headers['x-webhooks-signature'];
  const timestamp = req.headers['x-webhooks-timestamp'];

  if (!webhookId || !signatureHeader || !timestamp) {
    return res.status(401).json({ error: 'Missing required headers' });
  }

  const signature = signatureHeader.replace(/^v\d+=/, '');
  const payload = req.body.toString();
  const webhookSecret = process.env.WEBHOOK_SECRET;

  if (
    !webhookSecret ||
    !verifyWebhookSignature(
      webhookId,
      payload,
      signature,
      timestamp,
      webhookSecret
    )
  ) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(payload);
  // Process the webhook event
  res.status(200).json({ received: true });
});

Body

application/json
  • Execution: Completed
  • Execution: Downloads Complete

A webhook event payload containing event type and data

type
string
required
Allowed value: "execution.complete"
data
Completed Execution · object
required

Data about the completed execution.

  • Completed Execution
  • Failed Execution

Response

200

Webhook received successfully