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

# Video Rendering

> Render quiz videos asynchronously and download the output

## 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

<Note>
  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`.
</Note>

## Create a render job

```bash theme={null}
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:

```json theme={null}
{
  "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:

```bash theme={null}
curl https://app.quiz-quail.com/api/v1/renders/job_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "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:

```bash theme={null}
curl https://app.quiz-quail.com/api/v1/renders/job_abc123/download \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "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.

```bash theme={null}
curl -X DELETE https://app.quiz-quail.com/api/v1/renders/job_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "data": { "cancelled": true }
}
```

## List render jobs

Retrieve your render history with optional filters:

```bash theme={null}
curl "https://app.quiz-quail.com/api/v1/renders?quiz_id=QUIZ_ID&status=done&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "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](/webhooks) for setup details.

### `render.completed`

Sent when a render finishes successfully:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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.

```javascript theme={null}
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;
}
```

<Info>
  **Required scope:** `renders:read` for polling and downloading, `renders:write`
  for creating and cancelling render jobs.
</Info>
