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