Skip to main content

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:
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):
{
  "data": {
    "id": "wh_abc123",
    "url": "https://example.com/webhooks/quizquail",
    "events": ["render.completed", "render.failed"],
    "secret": "whsec_k7x9m2p4q8r1t6v3y0..."
  }
}
The secret is only returned once at creation time. Store it securely — you will need it to verify webhook signatures.

Managing Webhooks

MethodEndpointDescription
GET/api/v1/webhooksList all webhooks (secrets excluded)
POST/api/v1/webhooksRegister a new webhook
DELETE/api/v1/webhooks/{id}Delete a webhook
POST/api/v1/webhooks/{id}/testSend a test event

Event Types

EventDescription
render.completedA video render finished successfully.
render.failedA video render failed.
youtube.uploadedA video was uploaded to YouTube.
youtube.failedA YouTube upload failed.

Delivery Format

When an event fires, Quiz Quail sends a POST request to your webhook URL with these headers:
HeaderDescription
Content-Typeapplication/json
X-QuizQuail-EventEvent name (e.g. render.completed)
X-QuizQuail-SignatureHMAC-SHA256 signature: sha256=<hex>
X-QuizQuail-DeliveryUnique 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

{
  "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

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

youtube.uploaded

{
  "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

{
  "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

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

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