Webhooks

Async webhook delivery, HMAC-SHA256 signature verification, and callback replay for component executions.

Webhooks

When a callbackUrl is provided on execution, Zafeguard delivers the completed execution record to your endpoint as an HTTP POST with a JSON body. This page covers how to secure, verify, and recover webhook deliveries.


How it works

POST /v1/sdk/components
  callbackUrl: "https://your-app.com/webhooks/wr"
  callbackSecret: "my-hmac-secret"
  attempts: 2

        ▼  (component runs in background)

        ▼  on completion
Zafeguard ──POST──▶  https://your-app.com/webhooks/wr
                         x-delivery-id: <executionId>:<attemptNumber>
                         x-event: component.execution.terminal
                         x-signature-version: v1
                         x-signature-timestamp: <ISO-8601>
                         x-signature: sha256=<hex>
                         Body: { execution record }

The payload is the same execution record shape returned by Execute Component.

Callback headers

HeaderValueDescription
x-delivery-id{executionId}:{attemptNumber}Identifies this specific delivery attempt
x-eventcomponent.execution.terminalEvent type
x-signature-versionv1Signature algorithm version (only present when callbackSecret is set)
x-signature-timestampISO-8601 stringTimestamp used to construct the signature
x-signaturesha256=<hex>HMAC-SHA256 signature (only present when callbackSecret is set)

Signature verification

When callbackSecret is set, Zafeguard signs {x-signature-timestamp}.{rawBody} using HMAC-SHA256 and sends the result in the x-signature header as sha256=<hex>. The timestamp is an ISO-8601 string sent in x-signature-timestamp.

Always verify this signature before processing the payload.

Verifying in Node.js

import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhookSignature(
  rawBody: Buffer,
  headers: Record<string, string | string[] | undefined>,
  secret: string,
): boolean {
  const timestamp = headers['x-signature-timestamp'] as string;
  const signatureHeader = headers['x-signature'] as string;

  const signedPayload = `${timestamp}.${rawBody.toString()}`;
  const expected = createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  const received = signatureHeader.replace('sha256=', '');

  return timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex'),
  );
}

// Express handler example
app.post('/webhooks/wr', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyWebhookSignature(req.body, req.headers, process.env.ZAFEGUARD_CALLBACK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }

  const execution = JSON.parse(req.body.toString());
  console.log('Execution completed:', execution.id, execution.status);
  res.sendStatus(200);
});

Always use timingSafeEqual to compare signatures. Standard string comparison (===) is vulnerable to timing attacks.

Verifying in Python

import hmac
import hashlib

def verify_signature(raw_body: bytes, headers: dict, secret: str) -> bool:
    timestamp = headers['x-signature-timestamp']
    signed_payload = f"{timestamp}.{raw_body.decode()}"
    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256,
    ).hexdigest()
    received = headers['x-signature'].replace('sha256=', '')
    return hmac.compare_digest(expected, received)

Retry behaviour

FieldDefaultDescription
attempts1Total delivery attempts (max 3)
callbackHeaders{}Custom headers added to every delivery request

If a delivery fails (non-2xx response or timeout), Zafeguard retries up to the configured attempts count. Retries are spaced apart automatically.


Replay callback

POST /v1/sdk/components/executions/:executionId/replay-callback

Manually re-deliver the completion webhook for a finished execution. Useful when your endpoint was temporarily unavailable or returned an error. Idempotent — safe to call multiple times.

curl -X POST \
  https://api.zafeguard.com/v1/sdk/components/executions/3f7a.../replay-callback \
  -H "X-Api-Key: ws_..." \
  -H "X-Sdk-Timestamp: <timestamp>" \
  -H "X-Sdk-Signature: <signature>"

Response — same execution record shape as Execute Component.

On this page