Skip to main content
By the end of this guide you’ll have:
  • A live webhook endpoint receiving Kynva events.
  • HMAC signature verification.
  • Idempotent processing (safe to retry).
  • A DLQ replay flow for the rare event that all retries fail.

Step 1 — stand up an endpoint

Pick any framework. Here’s Express:
import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.KYNVA_WEBHOOK_SECRET!;

app.post(
  "/webhooks/kynva",
  express.raw({ type: "application/json" }), // raw bytes — required for HMAC
  async (req, res) => {
    const sig = (req.header("x-renderforge-signature") ?? "").replace(/^sha256=/, "");
    const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");

    if (
      sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))
    ) {
      return res.status(401).send("bad signature");
    }

    const payload = JSON.parse(req.body.toString("utf8"));
    // ... dispatch on payload.event ...
    res.sendStatus(200);
  },
);

app.listen(3000);
More languages on the verification examples page.

Step 2 — register it with Kynva

curl -X POST https://api.kynva.ai/api/v1/facade/webhooks \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://your-app.com/webhooks/kynva",
    "events": ["render.completed", "render.failed", "batch.completed"],
    "description": "Production"
  }'
The response includes the secret — save it to your secret manager. You won’t see it again.
{
  "id": "wh_01HV...",
  "url": "https://your-app.com/webhooks/kynva",
  "secret": "whsec_4f7c0a...",
  "events": ["render.completed", "render.failed", "batch.completed"],
  "active": true
}

Step 3 — send a test delivery

curl -X POST "https://api.kynva.ai/api/v1/facade/webhooks/wh_01HV.../test" \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
Kynva POSTs a synthetic render.completed to your URL. Confirm your endpoint:
  1. Returned 200.
  2. Logged the verified signature.
  3. Parsed the payload without errors.

Step 4 — dedupe deliveries

Retries can deliver the same event twice. Dedupe on X-RenderForge-Delivery:
const deliveryId = req.header("x-renderforge-delivery")!;
if (await redis.set(`webhook:seen:${deliveryId}`, "1", "NX", "EX", 86400) === null) {
  return res.status(200).end(); // already processed
}
SET NX is atomic — only the first request gets through.

Step 5 — make processing safe to fail

If your handler crashes after returning 200, Kynva won’t retry — it considers the event delivered. So either:
  • Do all the work before returning 200 (simple, slower), or
  • Enqueue to your own background queue and return 200 immediately (fast, more moving parts).
If you go with the queue: make sure the enqueue is the only thing that can fail before you 200. Otherwise you’ll silently lose events.

Step 6 — replay from the DLQ when something goes wrong

If your endpoint is down past the last retry (2 hours after first attempt), events land in the dead-letter queue. They’re kept for 14 days.
# List failed deliveries
curl "https://api.kynva.ai/api/v1/facade/webhooks/dlq?webhook_id=wh_01HV..." \
  -H "Authorization: Bearer $KYNVA_API_KEY"

# Replay one
curl -X POST "https://api.kynva.ai/api/v1/facade/webhooks/dlq/{event_id}/retry" \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
The DLQ is your safety net for outages on your side. Don’t rely on it for normal flow.

Step 7 — rotate the secret periodically

Every 90 days, or after any suspected leak:
curl -X POST "https://api.kynva.ai/api/v1/facade/webhooks/wh_01HV.../rotate-secret" \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
The old secret keeps working for 24 hours. Verify against both old and new during that window so you can roll deploys without webhook downtime.

What to do next

Full event reference

Security details