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

# Webhooks

> Receive real-time notifications when renders complete or YouTube uploads finish.

## Overview

Webhooks let your application receive HTTP callbacks when asynchronous events happen in Quiz Quail — such as a video render completing or a YouTube upload finishing. Instead of polling for status, register a webhook URL and Quiz Quail will `POST` event payloads to it in real time.

## Registering a Webhook

Create a webhook by specifying a URL and the events you want to subscribe to:

```bash theme={null}
curl -X POST https://app.quiz-quail.com/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/quizquail",
    "events": ["render.completed", "render.failed"]
  }'
```

**Response (201 Created):**

```json theme={null}
{
  "data": {
    "id": "wh_abc123",
    "url": "https://example.com/webhooks/quizquail",
    "events": ["render.completed", "render.failed"],
    "secret": "whsec_k7x9m2p4q8r1t6v3y0..."
  }
}
```

<Warning>
  The `secret` is only returned once at creation time. Store it securely — you will need it to [verify webhook signatures](#verifying-signatures).
</Warning>

## Managing Webhooks

| Method   | Endpoint                     | Description                          |
| -------- | ---------------------------- | ------------------------------------ |
| `GET`    | `/api/v1/webhooks`           | List all webhooks (secrets excluded) |
| `POST`   | `/api/v1/webhooks`           | Register a new webhook               |
| `DELETE` | `/api/v1/webhooks/{id}`      | Delete a webhook                     |
| `POST`   | `/api/v1/webhooks/{id}/test` | Send a test event                    |

## Event Types

| Event              | Description                           |
| ------------------ | ------------------------------------- |
| `render.completed` | A video render finished successfully. |
| `render.failed`    | A video render failed.                |
| `youtube.uploaded` | A video was uploaded to YouTube.      |
| `youtube.failed`   | A YouTube upload failed.              |

## Delivery Format

When an event fires, Quiz Quail sends a `POST` request to your webhook URL with these headers:

| Header                  | Description                           |
| ----------------------- | ------------------------------------- |
| `Content-Type`          | `application/json`                    |
| `X-QuizQuail-Event`     | Event name (e.g. `render.completed`)  |
| `X-QuizQuail-Signature` | HMAC-SHA256 signature: `sha256=<hex>` |
| `X-QuizQuail-Delivery`  | Unique delivery ID for idempotency    |

Deliveries have a **10-second timeout**. Your endpoint should return a `2xx` status code to acknowledge receipt.

## Event Payloads

### render.completed

```json theme={null}
{
  "render_id": "rnd_abc123",
  "quiz_id": "quiz_def456",
  "status": "completed",
  "download_url": "https://storage.quizquail.com/renders/rnd_abc123.mp4",
  "duration_ms": 45200,
  "completed_at": "2026-03-15T14:30:00Z"
}
```

### render.failed

```json theme={null}
{
  "render_id": "rnd_abc123",
  "quiz_id": "quiz_def456",
  "status": "failed",
  "error": "Composition exceeded maximum duration",
  "failed_at": "2026-03-15T14:30:00Z"
}
```

### youtube.uploaded

```json theme={null}
{
  "upload_id": "upl_abc123",
  "quiz_id": "quiz_def456",
  "youtube_video_id": "dQw4w9WgXcQ",
  "youtube_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
  "title": "World Capitals Quiz | Can You Name Them All?",
  "uploaded_at": "2026-03-15T15:00:00Z"
}
```

### youtube.failed

```json theme={null}
{
  "upload_id": "upl_abc123",
  "quiz_id": "quiz_def456",
  "error": "YouTube API quota exceeded",
  "failed_at": "2026-03-15T15:00:00Z"
}
```

## Verifying Signatures

Every webhook delivery is signed with your webhook secret using HMAC-SHA256. **Always verify signatures** to ensure the payload was sent by Quiz Quail and has not been tampered with.

The signature is in the `X-QuizQuail-Signature` header, formatted as `sha256=<hex_digest>`. To verify, compute the HMAC-SHA256 of the raw request body using your webhook secret and compare it to the signature.

### Node.js Example

```javascript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhookSignature(rawBody, signatureHeader, secret) {
  // signatureHeader is "sha256=<hex>"
  const expectedSignature = signatureHeader;
  if (!expectedSignature || !expectedSignature.startsWith("sha256=")) {
    return false;
  }

  const expectedHex = expectedSignature.slice("sha256=".length);

  const computedHex = createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  // Use timing-safe comparison to prevent timing attacks
  const expected = Buffer.from(expectedHex, "hex");
  const computed = Buffer.from(computedHex, "hex");

  if (expected.length !== computed.length) {
    return false;
  }

  return timingSafeEqual(expected, computed);
}
```

### Express.js Middleware

```javascript theme={null}
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();

// Use raw body for signature verification
app.post(
  "/webhooks/quizquail",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-quizquail-signature"];
    const event = req.headers["x-quizquail-event"];
    const deliveryId = req.headers["x-quizquail-delivery"];
    const secret = process.env.QUIZQUAIL_WEBHOOK_SECRET;

    // Verify signature
    if (!verifyWebhookSignature(req.body, signature, secret)) {
      console.error("Invalid webhook signature");
      return res.status(401).send("Invalid signature");
    }

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

    // Handle the event
    switch (event) {
      case "render.completed":
        console.log(`Render ${payload.render_id} completed`);
        console.log(`Download: ${payload.download_url}`);
        break;
      case "render.failed":
        console.error(`Render ${payload.render_id} failed: ${payload.error}`);
        break;
      case "youtube.uploaded":
        console.log(`Video uploaded: ${payload.youtube_url}`);
        break;
      case "youtube.failed":
        console.error(`Upload failed: ${payload.error}`);
        break;
    }

    // Acknowledge receipt
    res.status(200).send("OK");
  },
);
```

## Testing Webhooks

Use the test endpoint to send a sample event to your webhook URL without triggering a real render or upload:

```bash theme={null}
curl -X POST https://app.quiz-quail.com/api/v1/webhooks/wh_abc123/test \
  -H "Authorization: Bearer YOUR_API_KEY"
```

This sends a test delivery to your registered URL with a sample payload. Use it to verify your endpoint is reachable and your signature verification is working.

## Best Practices

* **Respond quickly.** Return a `2xx` within 10 seconds. Offload heavy processing to a background job.
* **Use the delivery ID for idempotency.** Store `X-QuizQuail-Delivery` values and skip duplicates in case of retries.
* **Always verify signatures.** Never trust webhook payloads without verifying the HMAC signature.
* **Use HTTPS endpoints.** Always use `https://` URLs for production webhook endpoints.
