> ## 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.

# Verification examples

> Drop-in code to verify a Kynva webhook signature in Node, Python, Go, and bash.

Every Kynva webhook is signed with HMAC-SHA256 over the raw request body using the secret you got when you created the webhook. Verify before doing anything with the payload.

## Node (Express)

```typescript Node.js + Express theme={null}
import express from "express";
import crypto from "node:crypto";

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

// IMPORTANT: capture the raw body, not the parsed JSON. The signature is over bytes.
app.post(
  "/webhooks/kynva",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = (req.header("x-renderforge-signature") ?? "").replace(/^sha256=/, "");
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)            // Buffer of raw bytes
      .digest("hex");

    const ok =
      signature.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(signature, "hex"),
        Buffer.from(expected, "hex"),
      );

    if (!ok) {
      return res.status(401).send("bad signature");
    }

    const payload = JSON.parse(req.body.toString("utf8"));

    // Dedupe at-least-once delivery
    const deliveryId = req.header("x-renderforge-delivery");
    // ... check Redis / DB, return 200 if already processed ...

    switch (payload.event) {
      case "render.completed":
        // do your thing
        break;
      case "render.failed":
        // alert someone
        break;
    }

    res.sendStatus(200);
  },
);

app.listen(3000);
```

## Python (FastAPI)

```python Python + FastAPI theme={null}
import hmac, hashlib, json, os
from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = os.environ["KYNVA_WEBHOOK_SECRET"]

@app.post("/webhooks/kynva")
async def kynva_webhook(
    request: Request,
    x_renderforge_signature: str = Header(...),
    x_renderforge_delivery: str = Header(...),
):
    raw = await request.body()  # raw bytes — required for HMAC

    signature = x_renderforge_signature.removeprefix("sha256=")
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        raw,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="bad signature")

    payload = json.loads(raw)

    # Dedupe at-least-once delivery on x_renderforge_delivery here.

    if payload["event"] == "render.completed":
        # do your thing
        pass

    return {"ok": True}
```

## Go (net/http)

```go Go theme={null}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"strings"
)

var webhookSecret = os.Getenv("KYNVA_WEBHOOK_SECRET")

func handle(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "bad body", http.StatusBadRequest)
		return
	}

	signature := strings.TrimPrefix(r.Header.Get("X-RenderForge-Signature"), "sha256=")

	mac := hmac.New(sha256.New, []byte(webhookSecret))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))

	if !hmac.Equal([]byte(signature), []byte(expected)) {
		http.Error(w, "bad signature", http.StatusUnauthorized)
		return
	}

	var payload struct {
		Event string `json:"event"`
	}
	_ = json.Unmarshal(body, &payload)

	// Dedupe on r.Header.Get("X-RenderForge-Delivery") here.

	w.WriteHeader(http.StatusOK)
}

func main() {
	http.HandleFunc("/webhooks/kynva", handle)
	http.ListenAndServe(":3000", nil)
}
```

## bash (manual verify of a captured request)

Useful for debugging. Replay the raw body and signature you captured:

```bash theme={null}
body='{"event":"render.completed",...}'
secret='whsec_...'
signature_from_header='sha256=4f7c0a...'

expected="sha256=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$secret" -hex | awk '{print $2}')"

if [ "$expected" = "$signature_from_header" ]; then
  echo "signature OK"
else
  echo "signature MISMATCH"
fi
```

## Testing your endpoint without writing code

Trigger a test delivery:

```bash theme={null}
curl -X POST https://api.kynva.ai/api/v1/facade/webhooks/{webhook_id}/test \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

Kynva will POST a synthetic `render.completed` to your URL. Use this to confirm your signature verification path works end-to-end before going live.

## Common mistakes

| Symptom                               | Cause                                                                                                                                          |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `signature mismatch` on every request | You're hashing the **parsed-and-restringified** body, not the raw bytes. JSON formatting differences break HMAC.                               |
| Works locally, fails in prod          | A proxy/CDN in front of your server is re-encoding the body (e.g., adding a trailing newline). Bypass or configure pass-through.               |
| Only works some of the time           | Constant-time comparison missing → you're getting false rejects from string equality on near-misses. Use `timingSafeEqual` / `compare_digest`. |
| Works in dev, fails after rotation    | You updated the secret in only one place. Verify against **both** old and new during the 24h rotation grace.                                   |
