Skip to main content

Documentation Index

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

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

Signature verification

Every webhook request from Zenstep includes an X-Zenstep-Signature header containing an HMAC-SHA256 signature of the raw request body. You should verify this signature before processing any webhook payload.
X-Zenstep-Signature: sha256=abc123def456...

Getting your webhook secret

When you create a webhook endpoint in the dashboard (Settings → Notifications → Add webhook), Zenstep generates a unique secret for that endpoint. Copy the secret immediately — it is only shown once. If you lose the secret, rotate it from the webhook settings page. All subsequent deliveries will use the new secret.

Verifying the signature

const crypto = require("crypto");

function verifyZenstepSignature(payload, signature, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(payload, "utf8").digest("hex");

  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express.js handler
app.post(
  "/webhooks/zenstep",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-zenstep-signature"];
    const secret = process.env.ZENSTEP_WEBHOOK_SECRET;

    if (!verifyZenstepSignature(req.body, signature, secret)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.body);
    // Process event...
    res.sendStatus(200);
  },
);

Security best practices

Always verify signatures — do not process webhook payloads without signature verification, even in development. Use the raw request body — compute the HMAC over the raw bytes of the request body before JSON parsing. Whitespace differences will cause signature mismatches. Use timing-safe comparison — always use crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hmac.Equal (Go) to compare the computed and received signatures. Standard string comparison is vulnerable to timing attacks. Store the secret in an environment variable — never hardcode the webhook secret in your source code. Reject requests without the header — if X-Zenstep-Signature is missing, return 401 immediately.

Replay attacks

To prevent replay attacks, Zenstep includes a timestamp field in every payload. Reject payloads where the timestamp is more than 5 minutes in the past:
function isNotReplay(payload) {
  const timestamp = new Date(payload.timestamp);
  const ageMs = Date.now() - timestamp.getTime();
  return ageMs < 5 * 60 * 1000; // 5 minutes
}
Each delivery also has a unique id field. If your application requires strict idempotency, store processed delivery IDs and skip re-processing.