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.
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
Make sure everything is wired up by listing your quizzes.
curl https://app.quiz-quail.com/api/v1/quizzes \
-H "Authorization: Bearer YOUR_API_KEY"
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`);
You should see a response like this:
{
"data": [],
"pagination": {
"cursor": null,
"hasMore": false,
"total": 0
}
}
If you get a 401, double-check your API key. If you get a 403, make sure your key has the quizzes:read scope.
Let’s create your first quiz. The only required field is title (and even that defaults to “Untitled Quiz” if omitted).
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"}'
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);
The response includes the full quiz with a default “Round 1” already created:
{
"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..."
}
}
Save the quiz.id and rounds[0].id — you’ll need them in the next steps.
Now let’s add a multiple-choice question to Round 1. Replace QUIZ_ID and ROUND_ID with the IDs from the previous response.
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."
}'
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);
{
"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
}
}
Quiz Quail supports 13 question types. Here are a few to try:
| Type | Description |
|---|
multiple_choice | Classic A/B/C/D with one correct answer |
true_false | True or false statement |
image | Image-based multiple choice |
ranking | Put items in the correct order |
open_reveal | Open-ended with a reveal answer |
emoji | Guess from emoji clues |
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.
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"}'
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);
{
"data": {
"id": "render-abc-...",
"quiz_id": "abc123-...",
"status": "pending",
"output_url": null,
"error_message": null,
"created_at": "2026-03-15T...",
"completed_at": null
}
}
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.
The render job moves through these statuses: pending -> claimed -> rendering -> done (or failed). Poll the status endpoint until it completes.
# 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
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);
Instead of polling, you can set up a webhook to receive a render.completed or render.failed event when the job finishes. Once the render status is done, grab a signed download URL. The URL is valid for 1 hour.
curl https://app.quiz-quail.com/api/v1/renders/RENDER_ID/download \
-H "Authorization: Bearer YOUR_API_KEY"
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");
{
"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:
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: