Skip to main content
Your first API call is moments away. This guide walks you through creating a quiz, adding questions, rendering it to video, and downloading the result — all from the command line or your own code.

Prerequisites

Your API key is a secret. Never expose it in client-side code, public repos, or browser requests. Always send it server-side via the Authorization header.

Base URL

All API requests are made to:
https://app.quiz-quail.com/api/v1
Authenticate every request with the Authorization: Bearer header:
Authorization: Bearer YOUR_API_KEY

1
Verify your key
2
Make sure everything is wired up by listing your quizzes.
3
curl
curl https://app.quiz-quail.com/api/v1/quizzes \
  -H "Authorization: Bearer YOUR_API_KEY"
fetch
const res = await fetch("https://app.quiz-quail.com/api/v1/quizzes", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const { data, pagination } = await res.json();
console.log(`You have ${pagination.total} quizzes`);
4
You should see a response like this:
5
{
  "data": [],
  "pagination": {
    "cursor": null,
    "hasMore": false,
    "total": 0
  }
}
6
If you get a 401, double-check your API key. If you get a 403, make sure your key has the quizzes:read scope.
7
Create a quiz
8
Let’s create your first quiz. The only required field is title (and even that defaults to “Untitled Quiz” if omitted).
9
curl
curl -X POST https://app.quiz-quail.com/api/v1/quizzes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "My First API Quiz"}'
fetch
const res = await fetch("https://app.quiz-quail.com/api/v1/quizzes", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ title: "My First API Quiz" }),
});
const { data: quiz } = await res.json();
console.log("Quiz created:", quiz.id);
10
The response includes the full quiz with a default “Round 1” already created:
11
{
  "data": {
    "id": "abc123-...",
    "title": "My First API Quiz",
    "status": "draft",
    "rounds": [
      {
        "id": "round-456-...",
        "title": "Round 1",
        "order": 0,
        "questions": []
      }
    ],
    "themes": null,
    "created_at": "2026-03-15T...",
    "updated_at": "2026-03-15T..."
  }
}
12
Save the quiz.id and rounds[0].id — you’ll need them in the next steps.
13
Add questions
14
Now let’s add a multiple-choice question to Round 1. Replace QUIZ_ID and ROUND_ID with the IDs from the previous response.
15
curl
curl -X POST https://app.quiz-quail.com/api/v1/quizzes/QUIZ_ID/rounds/ROUND_ID/questions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "question_type": "multiple_choice",
    "content": {
      "text": "What is the largest planet in our solar system?",
      "options": ["Mars", "Jupiter", "Saturn", "Neptune"],
      "correctOptionIndex": 1
    },
    "hint": "It has a famous Great Red Spot.",
    "fact": "Jupiter is so large that over 1,300 Earths could fit inside it."
  }'
fetch
const res = await fetch(
  `https://app.quiz-quail.com/api/v1/quizzes/${quizId}/rounds/${roundId}/questions`,
  {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      question_type: "multiple_choice",
      content: {
        text: "What is the largest planet in our solar system?",
        options: ["Mars", "Jupiter", "Saturn", "Neptune"],
        correctOptionIndex: 1,
      },
      hint: "It has a famous Great Red Spot.",
      fact: "Jupiter is so large that over 1,300 Earths could fit inside it.",
    }),
  }
);
const { data: question } = await res.json();
console.log("Question created:", question.id);
16
{
  "data": {
    "id": "q-789-...",
    "question_type": "multiple_choice",
    "content": {
      "text": "What is the largest planet in our solar system?",
      "options": ["Mars", "Jupiter", "Saturn", "Neptune"],
      "correctOptionIndex": 1
    },
    "hint": "It has a famous Great Red Spot.",
    "fact": "Jupiter is so large that over 1,300 Earths could fit inside it.",
    "countdown_seconds": 10,
    "difficulty": null,
    "order": 0
  }
}
17
Quiz Quail supports 13 question types. Here are a few to try:
18
TypeDescriptionmultiple_choiceClassic A/B/C/D with one correct answertrue_falseTrue or false statementimageImage-based multiple choicerankingPut items in the correct orderopen_revealOpen-ended with a reveal answeremojiGuess from emoji clues
19
See Quizzes Guide for the full list with content schemas.
20
Render to video
21
Once your quiz has at least one question, you can render it to video. This kicks off an asynchronous job — the API responds immediately with a 202 Accepted and a job ID you can poll.
22
curl
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"}'
fetch
const res = await fetch("https://app.quiz-quail.com/api/v1/renders", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ quiz_id: quizId }),
});
// 202 Accepted
const { data: job } = await res.json();
console.log("Render started:", job.id, "Status:", job.status);
23
{
  "data": {
    "id": "render-abc-...",
    "quiz_id": "abc123-...",
    "status": "pending",
    "output_url": null,
    "error_message": null,
    "created_at": "2026-03-15T...",
    "completed_at": null
  }
}
24
The render endpoint returns 202 Accepted (not 200 OK), along with a Location header pointing to the job’s status URL. Only one render can be in progress per quiz at a time — attempting a second returns a 409 Conflict.
25
Poll for completion
26
The render job moves through these statuses: pending -> claimed -> rendering -> done (or failed). Poll the status endpoint until it completes.
27
curl
# Poll every 5 seconds
while true; do
  STATUS=$(curl -s https://app.quiz-quail.com/api/v1/renders/RENDER_ID \
    -H "Authorization: Bearer YOUR_API_KEY" | jq -r '.data.status')
  echo "Status: $STATUS"
  if [ "$STATUS" = "done" ] || [ "$STATUS" = "failed" ]; then break; fi
  sleep 5
done
fetch
async function pollRender(jobId: string): Promise<string> {
  while (true) {
    const res = await fetch(
      `https://app.quiz-quail.com/api/v1/renders/${jobId}`,
      { headers: { Authorization: "Bearer YOUR_API_KEY" } }
    );
    const { data: job } = await res.json();
    console.log("Status:", job.status);

    if (job.status === "done" || job.status === "failed") {
      return job.status;
    }
    // Wait 5 seconds before polling again
    await new Promise((r) => setTimeout(r, 5000));
  }
}

await pollRender(job.id);
28
Instead of polling, you can set up a webhook to receive a render.completed or render.failed event when the job finishes.
29
Download the video
30
Once the render status is done, grab a signed download URL. The URL is valid for 1 hour.
31
curl
curl https://app.quiz-quail.com/api/v1/renders/RENDER_ID/download \
  -H "Authorization: Bearer YOUR_API_KEY"
fetch
const res = await fetch(
  `https://app.quiz-quail.com/api/v1/renders/${jobId}/download`,
  { headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
const { data } = await res.json();
console.log("Download URL:", data.url);
console.log("Expires in:", data.expires_in, "seconds");
32
{
  "data": {
    "url": "https://storage.quizquail.com/renders/render-abc-....mp4?token=...",
    "expires_in": 3600
  }
}

Full script

Here’s the entire flow as a single TypeScript script you can copy and run:
full-example.ts
const API_KEY = process.env.QUIZ_QUAIL_API_KEY!;
const BASE = "https://app.quiz-quail.com/api/v1";

const headers = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
};

// 1. Create a quiz
const quizRes = await fetch(`${BASE}/quizzes`, {
  method: "POST",
  headers,
  body: JSON.stringify({ title: "Solar System Trivia" }),
});
const { data: quiz } = await quizRes.json();
const roundId = quiz.rounds[0].id;
console.log("Quiz created:", quiz.id);

// 2. Add questions
const questions = [
  {
    question_type: "multiple_choice",
    content: {
      text: "What is the largest planet in our solar system?",
      options: ["Mars", "Jupiter", "Saturn", "Neptune"],
      correctOptionIndex: 1,
    },
    hint: "It has a famous Great Red Spot.",
  },
  {
    question_type: "true_false",
    content: {
      text: "Venus is the hottest planet in our solar system.",
      correctAnswer: true,
    },
    fact: "Venus has a thick atmosphere that traps heat, making it hotter than Mercury.",
  },
  {
    question_type: "multiple_choice",
    content: {
      text: "How many moons does Mars have?",
      options: ["0", "1", "2", "4"],
      correctOptionIndex: 2,
    },
  },
];

for (const q of questions) {
  await fetch(`${BASE}/quizzes/${quiz.id}/rounds/${roundId}/questions`, {
    method: "POST",
    headers,
    body: JSON.stringify(q),
  });
}
console.log(`Added ${questions.length} questions`);

// 3. Start a render
const renderRes = await fetch(`${BASE}/renders`, {
  method: "POST",
  headers,
  body: JSON.stringify({ quiz_id: quiz.id }),
});
const { data: job } = await renderRes.json();
console.log("Render started:", job.id);

// 4. Poll until done
let status = job.status;
while (status !== "done" && status !== "failed") {
  await new Promise((r) => setTimeout(r, 5000));
  const pollRes = await fetch(`${BASE}/renders/${job.id}`, { headers });
  const { data } = await pollRes.json();
  status = data.status;
  console.log("Status:", status);
}

// 5. Download
if (status === "done") {
  const dlRes = await fetch(`${BASE}/renders/${job.id}/download`, { headers });
  const { data } = await dlRes.json();
  console.log("Download your video:", data.url);
}

What’s next?

You just created a quiz, added questions, and rendered a video — all through the API. Here’s where to go from here: