Skip to main content
Webhooks reach a public endpoint on your infrastructure. Anyone on the internet can hit that URL — including bad actors trying to forge a render.completed event so you ship something they didn’t pay for. Three defenses, in order:
  1. Verify the HMAC signature on every request. Reject anything that doesn’t match.
  2. Rotate secrets periodically and immediately after any suspected leak.
  3. Treat the body as untrusted until verification passes.

How signing works

For every delivery, Kynva computes:
signature = HMAC_SHA256(secret, raw_request_body)
The hex-encoded result is sent in the X-RenderForge-Signature header, prefixed with sha256=:
X-RenderForge-Signature: sha256=4f7c0a...e9d
This is byte-for-byte the same construction as signPayload() in src/webhooks/signer.ts.

Verification rules

  1. Use the raw request body, not a parsed-and-restringified copy. Even a whitespace change breaks the signature.
  2. Compare in constant time — use crypto.timingSafeEqual (Node) or hmac.compare_digest (Python). String equality is vulnerable to timing attacks.
  3. Strip the sha256= prefix before comparing.
  4. Verify before processing anything from the body, including the event type.
Working code lives on the verification examples page.

Rotating secrets

When to rotate:
  • On a schedule (we recommend every 90 days).
  • Immediately if you suspect a leak.
  • When a team member with access to the secret leaves.
How to rotate:
curl -X POST https://api.kynva.ai/api/v1/facade/webhooks/{webhook_id}/rotate-secret \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
The response includes the new secret. The previous secret keeps working for 24 hours so you can roll deploys without a webhook outage. During that window, verify against both secrets — accept if either matches.

Replay protection

A captured legitimate webhook can be replayed. Two strategies:

Dedupe on X-RenderForge-Delivery

Every delivery has a unique ID. Store the ones you’ve seen for at least 24h (e.g., in Redis with a TTL) and reject duplicates:
const id = req.headers["x-renderforge-delivery"];
if (await redis.get(`webhook:seen:${id}`)) {
  return res.status(200).end(); // already handled; ack to stop retries
}
await redis.set(`webhook:seen:${id}`, "1", "EX", 86400);

Reject stale timestamps

Compare X-RenderForge-Timestamp to “now” and reject anything older than 5 minutes:
const ts = Date.parse(req.headers["x-renderforge-timestamp"] as string);
if (Math.abs(Date.now() - ts) > 5 * 60 * 1000) {
  return res.status(400).end();
}
Use both for defense in depth.

Network controls

Optional but useful:
  • Allowlist Kynva egress IPs at your firewall. The current set is published at status.kynva.ai/network.
  • Require TLS on your webhook URL — https://.... We refuse to create webhooks against plain HTTP in production.
  • Don’t put your webhook URL on the open internet under a guessable path. Add a long random segment, e.g. https://your-app.com/webhooks/kynva/8c4a....