Overview
Video rendering is an asynchronous process. You create a render job, poll for its
status, and download the finished video once it completes. The lifecycle is:
- Create a render job (
POST /renders) — returns 202 Accepted
- Poll the job status (
GET /renders/{id}) until status is "done"
- Download the video (
GET /renders/{id}/download) — returns a signed URL
Only one render can be in progress per quiz at a time. Creating a second render
for a quiz that already has an active job returns 409 Conflict.
Create a render job
curl -X POST https://app.quiz-quail.com/api/v1/renders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "quiz_id": "QUIZ_ID" }'
The response has status 202 Accepted and includes a Location header pointing
to the job URL:
{
"data": {
"id": "job_abc123",
"quiz_id": "QUIZ_ID",
"status": "pending",
"created_at": "2026-03-15T12:00:00Z"
}
}
Request body
| Field | Type | Required | Description |
|---|
quiz_id | string | Yes | UUID of the quiz to render |
format | string | No | Output format (default: mp4) |
resolution | string | No | Output resolution |
Poll for status
Poll the job endpoint to track progress:
curl https://app.quiz-quail.com/api/v1/renders/job_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "job_abc123",
"quiz_id": "QUIZ_ID",
"status": "rendering",
"progress": 45,
"created_at": "2026-03-15T12:00:00Z"
}
}
Job statuses
| Status | Description |
|---|
pending | Job created, waiting for a worker |
claimed | Worker has picked up the job |
rendering | Video is being rendered |
done | Render complete — ready to download |
failed | Render failed (see error_message) |
Download the video
Once the job reaches "done", fetch a signed download URL:
curl https://app.quiz-quail.com/api/v1/renders/job_abc123/download \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"url": "https://storage.example.com/renders/job_abc123.mp4?token=...",
"expires_in": 3600
}
}
The signed URL is valid for 1 hour. Request a new one if it expires.
Cancel a render
Cancel an in-flight render job with a DELETE request. Only jobs with status
pending, claimed, or rendering can be cancelled.
curl -X DELETE https://app.quiz-quail.com/api/v1/renders/job_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": { "cancelled": true }
}
List render jobs
Retrieve your render history with optional filters:
curl "https://app.quiz-quail.com/api/v1/renders?quiz_id=QUIZ_ID&status=done&limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": "job_abc123",
"quiz_id": "QUIZ_ID",
"status": "done",
"created_at": "2026-03-15T12:00:00Z"
}
],
"pagination": {
"cursor": "eyJjcm...",
"hasMore": false,
"total": 1
}
}
| Parameter | Type | Description |
|---|
quiz_id | string | Filter by quiz |
status | string | Filter by status (pending, done, failed) |
limit | number | Results per page (1–100, default 20) |
cursor | string | Pagination cursor from a previous response |
Using webhooks
Instead of polling, register a webhook to receive notifications when renders
complete or fail. See the Webhooks guide for setup details.
render.completed
Sent when a render finishes successfully:
{
"render_id": "job_abc123",
"quiz_id": "QUIZ_ID",
"status": "completed",
"download_url": "https://storage.example.com/renders/job_abc123.mp4",
"duration_ms": 45200,
"completed_at": "2026-03-15T12:02:30Z"
}
render.failed
Sent when a render fails:
{
"render_id": "job_abc123",
"quiz_id": "QUIZ_ID",
"status": "failed",
"error": "Composition exceeded memory limit",
"failed_at": "2026-03-15T12:01:15Z"
}
Full example: render and download
This example creates a render job, polls until completion, then downloads the video.
const API_KEY = "your_api_key";
const BASE = "https://app.quiz-quail.com/api/v1";
async function renderAndDownload(quizId) {
// 1. Create render job
const createRes = await fetch(`${BASE}/renders`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ quiz_id: quizId }),
});
if (createRes.status !== 202) {
throw new Error(`Failed to create render: ${createRes.status}`);
}
const { data: job } = await createRes.json();
console.log(`Render job created: ${job.id}`);
// 2. Poll until done
let status = job.status;
while (status !== "done" && status !== "failed") {
await new Promise((r) => setTimeout(r, 5000)); // wait 5s
const pollRes = await fetch(`${BASE}/renders/${job.id}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { data: updated } = await pollRes.json();
status = updated.status;
console.log(`Status: ${status}`);
}
if (status === "failed") {
throw new Error("Render failed");
}
// 3. Get download URL
const dlRes = await fetch(`${BASE}/renders/${job.id}/download`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { data: download } = await dlRes.json();
console.log(`Download URL: ${download.url}`);
return download.url;
}
Required scope: renders:read for polling and downloading, renders:write
for creating and cancelling render jobs.