> ## 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.

# Webhook Safety

> Defensive event-driven integration principles, automatic exponential retries, and strict signature validation boundaries.

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.

```mermaid theme={null}
flowchart TD
    Event["Webhook Event Triggered"] -->|"Format: t=timestamp,v1=signature"| Gateway["Zentra Webhook Engine"]
    Gateway -->|"POST /webhooks (First Attempt)"| App["Your Application Consumer"]
    
    App -->|"200 OK / 204 No Content"| Success["Mark Event as DELIVERED in Journal"]
    App -->|"5xx Error / Network Timeout"| RetryCheck{"Attempts < 5?"}
    
    RetryCheck -->|"Yes"| Backoff["Exponential Backoff Delay (2^attempt * 5s)"]
    Backoff -->|"POST /webhooks (Same Signature)"| App
    
    RetryCheck -->|"No"| DLQ["Dead Letter Log (Manual Replay)"]
    
    classDef app fill:#14161A,stroke:#2A2E35,stroke-width:1px,color:#F5F4F0
    classDef zentra fill:#2563FF,stroke:#AFC8FF,stroke-width:1px,color:#F5F4F0
    classDef proof fill:#2A2E35,stroke:#5770A0,stroke-width:1px,color:#F5F4F0
    
    class App app
    class Gateway,RetryCheck,Backoff zentra
    class Event,Success,DLQ proof
```

***

## Egress Webhook Safety Rules

Ensure your webhook receiver implements these four security policies:

<CardGroup cols={2}>
  <Card title="Cryptographic Verification" icon="signature">
    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.
  </Card>

  <Card title="Replay Attack Defense" icon="hourglass-clock">
    Reject requests with timestamps that fall outside a **5-minute (300-second) tolerance window** to prevent attackers from intercepting and re-submitting events.
  </Card>

  <Card title="Idempotent Deduplication" icon="shield-check">
    Webhooks can be delivered more than once. Always store processed event IDs (`evt_...`) in a database table to avoid dual-processing.
  </Card>

  <Card title="Return Status Codes Only" icon="brackets-curly">
    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.
  </Card>
</CardGroup>

***

## 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:

```http theme={null}
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.

<CodeGroup>
  ```javascript Node.js theme={null}
  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();
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import json

  def verify_zentra_webhook(request_body: str, signature_header: str, secret: str) -> bool:
      if not signature_header:
          raise ValueError("Missing signature header")

      # 1. Parse t and v1 parameters
      parts = {}
      for part in signature_header.split(','):
          key, val = part.strip().split('=', 1)
          parts[key] = val

      timestamp = parts.get('t')
      signature = parts.get('v1')

      if not timestamp or not signature:
          raise ValueError("Invalid signature format")

      # 2. Validate timestamp drift (prevent replay attacks)
      now = int(time.time())
      drift_limit = 300  # 5 minutes
      if abs(now - int(timestamp)) > drift_limit:
          raise ValueError("Timestamp out of bounds")

      # 3. Compute expected signature
      payload = f"{timestamp}.{request_body}".encode('utf-8')
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256
      ).hexdigest()

      # 4. Secure, constant-time comparison
      if not hmac.compare_digest(signature, expected_signature):
          raise ValueError("Signature verification failed")

      return True
  ```
</CodeGroup>

<Warning>
  Never rely solely on webhook arrival to confirm a change in balance. Always verify the transaction context by using the [API state endpoint](/api-reference/transfers/overview) or cross-referencing your immutable ledger logs.
</Warning>
