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

# Set up webhooks end-to-end

> Create a webhook, verify signatures, handle retries, and replay from the DLQ — in 10 minutes.

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:

```typescript theme={null}
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](/webhooks/verification-examples) page.

## Step 2 — register it with Kynva

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

```json theme={null}
{
  "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

```bash theme={null}
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`:

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

```bash theme={null}
# 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:

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

<CardGroup cols={2}>
  <Card title="Full event reference" icon="bolt" href="/webhooks/events" />

  <Card title="Security details" icon="shield-halved" href="/webhooks/security" />
</CardGroup>
