Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.usezentra.com/llms.txt

Use this file to discover all available pages before exploring further.

In distributed financial infrastructure, webhooks represent critical, event-driven integration boundaries. Because the network path is untrusted and packets can be dropped, delayed, or replayed, your application must treat every incoming webhook callback as a formal, untrusted input. The Zentra Control Plane delivers every ledger and transfer mutation as a signed, signature-verifiable webhook event. By adhering to defensive integration rules, you can process incoming events with absolute security and consistency.

The Retry-Aware Delivery Flow

If your application consumer is down, experiencing transient database locks, or failing to respond in time, Zentra does not drop events. We execute an automated retry schedule using exponential backoff to guarantee delivery.

Egress Webhook Safety Rules

Ensure your webhook receiver implements these four security policies:

Cryptographic Verification

Compute the HMAC-SHA256 signature on the raw request body and compare it with the signature sent in the header before executing any business logic.

Replay Attack Defense

Reject requests with timestamps that fall outside a 5-minute (300-second) tolerance window to prevent attackers from intercepting and re-submitting events.

Idempotent Deduplication

Webhooks can be delivered more than once. Always store processed event IDs (evt_...) in a database table to avoid dual-processing.

Return Status Codes Only

Respond immediately with a 200 OK or 204 No Content. Do not block the connection with long-running business logic; process webhooks asynchronously in a queue.

Verifying Webhook Signatures

Zentra signs all egress webhook requests with a custom header: x-zentra-signature. The header contains a Unix timestamp and a cryptographic signature in a Stripe-style comma-separated key-value format:
x-zentra-signature: t=1779234850,v1=6a7b2cde3f901234abcd56789eff0123...
  • The t parameter represents the Unix epoch timestamp (in seconds) when the event was generated.
  • The v1 parameter represents the HMAC-SHA256 signature of the concatenated payload: timestamp.raw_body.

Implementation Code Examples

Initialize signature validation in your endpoint handler using constant-time string comparison to prevent timing attacks.
import crypto from "crypto";

export async function handleWebhook(req, res) {
  const signatureHeader = req.headers["x-zentra-signature"];
  const rawBody = req.rawBody; // Ensure you read the raw unparsed string body
  const endpointSecret = process.env.ZENTRA_WEBHOOK_SECRET;

  if (!signatureHeader) {
    return res.status(401).json({ error: "Missing signature" });
  }

  // 1. Parse t and v1 parameters
  const parts = Object.fromEntries(
    signatureHeader.split(",").map(part => part.trim().split("="))
  );

  const timestamp = parts.t;
  const signature = parts.v1;

  if (!timestamp || !signature) {
    return res.status(400).json({ error: "Invalid signature format" });
  }

  // 2. Validate timestamp drift (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  const driftLimit = 300; // 5 minutes
  if (Math.abs(now - parseInt(timestamp, 10)) > driftLimit) {
    return res.status(401).json({ error: "Timestamp out of bounds" });
  }

  // 3. Compute expected signature
  const payload = `${timestamp}.${rawBody}`;
  const expectedSignature = crypto
    .createHmac("sha256", endpointSecret)
    .update(payload)
    .digest("hex");

  // 4. Secure, constant-time comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expectedSignature, "hex")
  );

  if (!isValid) {
    return res.status(401).json({ error: "Signature verification failed" });
  }

  // 5. Deduplicate and enqueue event processing asynchronously
  const event = JSON.parse(rawBody);
  const wasProcessed = await deduplicateEvent(event.id);
  if (wasProcessed) {
    return res.status(200).send("Event already processed");
  }

  await enqueueEvent(event);
  return res.status(204).end();
}
Never rely solely on webhook arrival to confirm a change in balance. Always verify the transaction context by using the API state endpoint or cross-referencing your immutable ledger logs.