> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zerocash.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook secrets

> Verify that an incoming webhook genuinely came from Zerocash.

Zerocash signs every webhook request. Verifying that signature is what stops an attacker from
POSTing a fake "payment successful" event straight at your endpoint.

<Warning>
  Your webhook URL is public. Without signature verification, anyone who finds it can tell your
  system that a transaction succeeded. Verify before you process — always.
</Warning>

## Set up signatures

<Steps>
  <Step title="Generate a webhook secret">
    In your dashboard, go to **Developers → API Keys** and generate a webhook secret.
  </Step>

  <Step title="Store it securely">
    Put it in an environment variable or a secrets manager. Never commit it.
  </Step>

  <Step title="Read the header">
    Zerocash includes the signature in the `X-Webhook-Signature` header of every webhook request.
  </Step>
</Steps>

## Verify the signature

On each incoming webhook:

1. Read the signature from the `X-Webhook-Signature` header.
2. Compare it against your stored webhook secret.
3. Process the webhook **only** if they match.

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  // Constant-time comparison — a plain === leaks information through timing.
  function isValidSignature(received) {
    const expected = process.env.ZEROCASH_WEBHOOK_SECRET;
    if (!received || !expected) return false;

    const a = Buffer.from(received);
    const b = Buffer.from(expected);
    if (a.length !== b.length) return false;

    return crypto.timingSafeEqual(a, b);
  }

  app.post("/webhooks/zerocash", (req, res) => {
    if (!isValidSignature(req.headers["x-webhook-signature"])) {
      return res.status(401).send("Invalid signature");
    }

    queue.push(req.body);
    res.status(200).end();
  });
  ```

  ```python Python theme={null}
  import hmac
  import os
  from flask import Flask, request, abort

  app = Flask(__name__)

  def is_valid_signature(received: str | None) -> bool:
      expected = os.environ.get("ZEROCASH_WEBHOOK_SECRET")
      if not received or not expected:
          return False
      # compare_digest is constant-time
      return hmac.compare_digest(received, expected)

  @app.post("/webhooks/zerocash")
  def handle_webhook():
      if not is_valid_signature(request.headers.get("X-Webhook-Signature")):
          abort(401)

      enqueue(request.get_json())
      return "", 200
  ```

  ```php PHP theme={null}
  <?php
  function is_valid_signature(?string $received): bool {
      $expected = getenv('ZEROCASH_WEBHOOK_SECRET');
      if (!$received || !$expected) {
          return false;
      }
      // hash_equals is constant-time
      return hash_equals($expected, $received);
  }

  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;

  if (!is_valid_signature($signature)) {
      http_response_code(401);
      exit;
  }

  $payload = json_decode(file_get_contents('php://input'), true);
  enqueue($payload);
  http_response_code(200);
  ```
</CodeGroup>

<Note>
  Use a constant-time comparison (`timingSafeEqual`, `hmac.compare_digest`, `hash_equals`) rather
  than `==`. A naive comparison returns faster the earlier it finds a mismatched byte, which leaks
  enough information to recover the secret one character at a time.
</Note>

## Security best practices

<CardGroup cols={2}>
  <Card title="Always verify" icon="shield-check">
    Check the signature on every request, including ones you plan to ignore.
  </Card>

  <Card title="Keep the secret out of git" icon="lock">
    Environment variables or a secrets manager — never source control.
  </Card>

  <Card title="Rotate periodically" icon="rotate">
    Regenerate the secret on a schedule, and immediately if you suspect exposure.
  </Card>

  <Card title="HTTPS only" icon="globe">
    Receive webhooks over TLS so payloads and headers cannot be read in transit.
  </Card>
</CardGroup>

<Tip>
  Handle processing timeouts explicitly. If your handler cannot finish quickly, acknowledge with a
  `2xx` and finish the work in the background — see
  [Getting started](/guides/webhooks/getting-started).
</Tip>
