Skip to main content

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:
  1. Create a render job (POST /renders) — returns 202 Accepted
  2. Poll the job status (GET /renders/{id}) until status is "done"
  3. 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

FieldTypeRequiredDescription
quiz_idstringYesUUID of the quiz to render
formatstringNoOutput format (default: mp4)
resolutionstringNoOutput 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

StatusDescription
pendingJob created, waiting for a worker
claimedWorker has picked up the job
renderingVideo is being rendered
doneRender complete — ready to download
failedRender 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
  }
}
ParameterTypeDescription
quiz_idstringFilter by quiz
statusstringFilter by status (pending, done, failed)
limitnumberResults per page (1–100, default 20)
cursorstringPagination 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.