Which shape has three sides?
Website Embed
Interactive quiz inside a webpage.
Choose when: You want readers to play without leaving the page.
Implementation: Place the returned iframe HTML in your page or CMS template.
Atrivial API
Create quizzes from your content, publish them, and place them in webpages or email using embeds, links, or stable snippets.
Prefer a machine-readable contract? Download the OpenAPI 3 spec for SDK generation, Postman import, or contract tests.
Paste an existing Integrator API key below, or sign in to create a free Developer Starter key.
Sign in to claim a free Developer Starter key. No billing required.
Anti-abuse usage limits apply.
Manage or revoke keys in the Portal.
A Quiz owns Quiz Editions. Each edition contains ordered items; generation.itemType chooses trivia or poll, and generation.itemCount sets how many items the job should create.
/v1/quizzes/generate202Idempotency-Keyinput | Required source text, up to 4,000 characters. Markup is treated as literal source text; it is not rendered as HTML. Send a topic string, article excerpt, bullet list, or newsletter paragraph. Examples: "The Roman Empire", "Q3 earnings recap: revenue up 12%…", or a pasted 2–3 paragraph article section. |
|---|---|
generation (optional) | Optional per-request controls. Omit generation entirely to default to 4 total trivia items. Include generation when you need to choose the item type, item total, or guidance. |
generation.itemType | Use trivia or poll for single-type generation. Required unless generation.itemPlan is used. |
generation.itemCount | Optional integer. Anti-abuse usage limits apply. Total items for this request, not per topic. Omitted inside generation defaults to 4. |
generation.guidance | Optional editorial instructions, up to 500 characters. Omit the key, or send a non-empty string — blank or whitespace-only values return 400 invalid_request. Examples: "Focus on military history", "Keep answers under five words", "Suitable for 8th graders". |
Unsupported generation fields | Unsupported generation keys return 400 invalid_request. |
Idempotency fingerprint | Idempotency replays include generation only when the client sends a generation key. Omitting generation does not fingerprint default item controls. |
publication.mode | Optional publication behavior. manual, the default, leaves the generated edition unpublished. publishWhenReady publishes it after generation succeeds. |
curl -X POST https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes/generate \
-H "Authorization: Bearer atriv_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"input":"The Roman Empire","generation":{"itemType":"trivia","itemCount":4}}'HTTP/1.1 202 Accepted
Location: /v1/generation-jobs/550e8400-e29b-41d4-a716-446655440000
Retry-After: 2
Content-Type: application/json
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"quizId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"editionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"publication": {
"mode": "manual",
"state": "unpublished"
}
}Generation runs in the background. Poll GET /v1/generation-jobs/:id until status is succeeded. Honor Retry-After when the header is present (typically 2 seconds while normalizing, queued, or running). Watch X-RateLimit-* headers and handle 429 backoff — see Generation jobs and Rate limits.
curl https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/generation-jobs/JOB_ID \
-H "Authorization: Bearer atriv_live_..."If publication.state is unpublished, publish via POST /v1/quizzes/:id/publish first. Use embed fields only when publication.state is live. If GET /v1/approved-domains returns an empty list, add the embed domain in the Portal before testing embeds outside Atrivial.
<!-- Use embed.iframeUrl or embed.iframeHtml once publication.state is live. -->
<iframe
src="{{ embed.iframeUrl }}"
width="100%"
height="640"
frameborder="0"
allow="web-share; clipboard-write"
title="Atrivial trivia game"
loading="lazy"
></iframe>Replace {{ embed.iframeUrl }} at render time with embed.iframeUrl from the publish or job response.
Run a real generation against the API. Each run uses generation capacity and counts toward safety brakes (concurrency, queue depth, per-job item caps). Successful runs publish when ready, so live/embed fields appear automatically.
trivia·Items per job:4·Key prefix:noneIntegrator API generation creates an unpublished edition. Publishing makes that edition live on the Quiz’s stable URL and embed.
Integrator API generation defaults to publication.mode: "manual", so you can review before publishing. Set publication.mode to "publishWhenReady" to publish after generation succeeds.
For manual publication, review and publish from Questions in the Portal or call POST /v1/quizzes/:quizId/publish. Email, schedule, and website setup are optional and do not block publication.
Configure schedules in the Portal. Schedule management is not exposed in Integrator API v1.
Examples
Production-shaped flows for common partner products. Use after the quickstart.
Which shape has three sides?
Interactive quiz inside a webpage.
Choose when: You want readers to play without leaving the page.
Implementation: Place the returned iframe HTML in your page or CMS template.
Which shape has three sides?
Static prompt block with links to the live quiz.
Choose when: You can update email HTML for each quiz or campaign.
Implementation: Paste the generated table block into your email template.
Which shape has three sides?
Fixed email snippet whose hosted quiz images can refresh.
Choose when: Changing email HTML is hard after setup.
Implementation: Paste the snippet once; Atrivial refreshes hosted image contents.
Let editors attach a live Atrivial quiz to each CMS entry. Run Atrivial API calls in CMS server code, store the quiz id or slug, then fetch the current embed during server rendering.
Decision: Let editors pick a live quiz, generate from CMS copy, or paste an existing Portal quiz id.
Keep the Integrator API key in a server-side secret or environment variable; never expose it to browser or page JavaScript. GET /v1/quizzes builds a live-Quiz picker. generateLiveQuizFromCms sends publication.mode publishWhenReady, honors Retry-After while polling the Generation Job with a 60-attempt default limit, and returns only after status is succeeded and publication.state is live. You can also paste an existing Portal quiz id.
Result: A selected live Quiz or a generated live Quiz with identifiers and attachable embed data.
function cmsApiError(response, body, fallbackMessage) {
const error = body?.error ?? body?.publication?.error;
const code = error?.code;
const message = error?.message ?? fallbackMessage;
const detail = code ? code + ": " + message : message;
const requestId = error?.requestId ?? response.headers.get("X-Request-Id");
return requestId ? detail + " (requestId: " + requestId + ")" : detail;
}
function cmsRetryAfterMs(value) {
const normalized = value?.trim();
if (!normalized) return null;
if (/^\d+$/.test(normalized)) {
const seconds = Number(normalized);
return Number.isSafeInteger(seconds) ? seconds * 1000 : null;
}
if (/^[+-]?\d/.test(normalized)) return null;
const retryAt = Date.parse(normalized);
return Number.isNaN(retryAt) ? null : Math.max(retryAt - Date.now(), 0);
}
function cmsWait(delayMs) {
return new Promise((resolve) => setTimeout(resolve, delayMs));
}
async function listLiveQuizzes(apiKey) {
if (!apiKey) throw new Error("ATRIVIAL_API_KEY is required server-side.");
const response = await fetch("https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes?status=live&limit=25", {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new Error(cmsApiError(response, body, "List quizzes failed (" + response.status + ")"));
}
return response.json();
}
async function generateLiveQuizFromCms({
apiKey,
input = "Article sidebar: Space exploration milestones",
itemCount = 8,
idempotencyKey = crypto.randomUUID(),
maxPollAttempts = 60,
}) {
if (!apiKey) throw new Error("ATRIVIAL_API_KEY is required server-side.");
const pollLimit =
Number.isSafeInteger(maxPollAttempts) && maxPollAttempts > 0
? Math.min(maxPollAttempts, 300)
: 60;
const startResponse = await fetch("https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes/generate", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
input,
generation: { itemType: "trivia", itemCount },
publication: { mode: "publishWhenReady" },
}),
});
const startBody = await startResponse.json().catch(() => null);
if (!startResponse.ok) {
throw new Error(
cmsApiError(startResponse, startBody, "Generate failed (" + startResponse.status + ")")
);
}
if (!startBody?.jobId) {
throw new Error(cmsApiError(startResponse, startBody, "Generate response omitted jobId."));
}
const location = startResponse.headers.get("Location");
const jobUrl = location?.startsWith("http")
? location
: "https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api" + (location
? (location.startsWith("/") ? "" : "/") + location
: "/v1/generation-jobs/" + encodeURIComponent(startBody.jobId));
let response = startResponse;
let job = startBody;
let pollAttempts = 0;
for (;;) {
switch (job?.status) {
case "normalizing":
case "queued":
case "running": {
for (;;) {
if (pollAttempts >= pollLimit) {
throw new Error(
cmsApiError(
response,
job,
"Generation Job timed out after " + pollLimit + " poll attempts."
)
);
}
const delayMs = cmsRetryAfterMs(response.headers.get("Retry-After")) ?? 2000;
await cmsWait(delayMs);
pollAttempts += 1;
response = await fetch(jobUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const pollBody = await response.json().catch(() => null);
if (response.ok) {
job = pollBody;
break;
}
const rateLimitDelayMs = cmsRetryAfterMs(response.headers.get("Retry-After"));
if (
response.status === 429 &&
pollBody?.error?.code === "rate_limited" &&
!pollBody?.limit &&
rateLimitDelayMs !== null
) {
continue;
}
throw new Error(
cmsApiError(
response,
pollBody,
"Generation Job request failed (" + response.status + ")"
)
);
}
break;
}
case "succeeded":
if (job.publication?.state !== "live") {
throw new Error(
cmsApiError(
response,
job,
"Generation succeeded without live publication (state: " +
(job.publication?.state ?? "missing") +
")."
)
);
}
if (
!job.quizId ||
!job.quizSlug ||
!job.embed?.playUrl ||
!job.embed?.iframeUrl ||
!job.embed?.iframeHtml
) {
throw new Error(
cmsApiError(response, job, "Live Generation Job omitted attachable embed data.")
);
}
return { quizId: job.quizId, quizSlug: job.quizSlug, embed: job.embed };
case "failed":
case "cancelled":
throw new Error(
cmsApiError(
response,
job,
"Generation Job ended with status " + String(job.status) + "."
)
);
default:
throw new Error(
cmsApiError(
response,
job,
"Generation Job returned unknown status " + String(job?.status) + "."
)
);
}
}
}Decision: Store quizId for stable API reads, quizSlug for slug embed reads, or both.
Persist atrivial_quiz_id and/or atrivial_quiz_slug on the CMS record (custom fields, meta box, or headless JSON).
Result: Each CMS entry knows which Atrivial quiz it should render.
{
"atrivial_quiz_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"atrivial_quiz_slug": "roman-empire-newsletter-march"
}Decision: Use GET /v1/quizzes/:id when you store ids, or GET /v1/embeds/:slug when slugs are the CMS contract.
Use an authenticated server-side GET during page build or render so iframeHtml / iframeUrl reflect the live published Quiz Edition.
Result: Readers can play the quiz in place on the article or module.
async function fetchQuizEmbed({ apiKey, quizId, quizSlug }) {
if (!apiKey) throw new Error("ATRIVIAL_API_KEY is required server-side.");
let url;
if (quizId) {
url = "https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes/" + encodeURIComponent(quizId);
} else if (quizSlug) {
url = "https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/embeds/" + encodeURIComponent(quizSlug);
} else {
throw new Error("Provide quizId or quizSlug.");
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const quiz = await response.json().catch(() => null);
if (!response.ok) {
const code = quiz?.error?.code;
const message = quiz?.error?.message ?? "Fetch quiz failed (" + response.status + ")";
const detail = code ? code + ": " + message : message;
const requestId = quiz?.error?.requestId ?? response.headers.get("X-Request-Id");
throw new Error(requestId ? detail + " (requestId: " + requestId + ")" : detail);
}
if (quiz.status !== "live" || !quiz.embed?.iframeHtml) {
const requestId = response.headers.get("X-Request-Id");
const message = "Quiz is not live or has no attachable embed.";
throw new Error(requestId ? message + " (requestId: " + requestId + ")" : message);
}
return quiz.embed.iframeHtml;
}Decision: Swap the stored quiz id or slug, or publish a new edition of the same Quiz.
The next render uses the new reference. If the Quiz itself changed, publish the new edition before expecting embeds to update.
Result: Editors can change the embedded quiz from the CMS workflow.
Production notes
Cache carefully | Cache GET /v1/quizzes/:id briefly; bust cache when editors republish or swap ids. |
|---|---|
Unpublished vs live | Unpublished editions are not player-visible. Do not render embed fields until publication.state is live. |
Idempotency | Idempotency-Key on generate from CMS saves prevents duplicate jobs on double-submit. |
Listing quizzes | Use GET /v1/quizzes for a simple picker; store the chosen id on each CMS entry. |
Approved domains | Embeds require approved partner domains (GET /v1/approved-domains). Use a staging domain for realistic tests; localhost is not a production-approved domain. |
Webhooks | Completion webhooks are not available in v1. Poll Generation Jobs from a backend worker for high-volume CMS generation. |
Reference
Generated from OpenAPI. Expand a group for paths.
/v1/me/v1/approved-domains/v1/quizzes/v1/quizzes/generate/v1/quizzes/{id}/v1/quizzes/{id}/editions/{editionId}/v1/quizzes/{id}/editions/{editionId}/v1/quizzes/{id}/editions/{editionId}/publish/v1/quizzes/{id}/generate/v1/quizzes/{id}/publish/v1/generation-jobs/{id}/v1/embeds/{slug}/v1/email-image-sets/v1/email-image-sets/{id}/v1/openapi.jsonOptional detail: auth, jobs, publishing, errors, limits, full spec.
Use Bearer atriv_live_… on every protected v1 request. GET /v1/openapi.json is public and does not require authentication. Keys are created and revoked in the Portal. Revoked keys return 401 invalid_api_key immediately.
Bearer token per account. Keys are created and revoked in the Portal.
401 invalid_api_key immediately.curl https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes/generate \
-H "Authorization: Bearer atriv_live_REPLACE_ME" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "input": "The Roman Empire", "generation": { "itemType": "trivia", "itemCount": 4 } }'Verify your API key and list approved embed domains
Use these read-only endpoints during setup and debugging before you generate or embed.
GET /v1/me | Returns the authenticated Portal account id, API key id, label, and key prefix. Use to verify which key is active or to log account context in your integration. |
|---|---|
GET /v1/approved-domains | Returns hostnames approved for website embeds and image-based email. If items is empty, add your site or staging domain in the Portal before embedding outside Atrivial. |
curl https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/me \
-H "Authorization: Bearer atriv_live_REPLACE_ME"curl https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/approved-domains \
-H "Authorization: Bearer atriv_live_REPLACE_ME"Start background generation, then poll until it finishes
Quiz generation runs in the background. POST /v1/quizzes/generate returns 202 with a job id and Location: /v1/generation-jobs/:id.
input | Required source text, up to 4,000 characters. Markup is treated as literal source text; it is not rendered as HTML. Send a topic string, article excerpt, bullet list, or newsletter paragraph. Examples: "The Roman Empire", "Q3 earnings recap: revenue up 12%…", or a pasted 2–3 paragraph article section. |
|---|---|
generation (optional) | Optional per-request controls. Omit generation entirely to default to 4 total trivia items. Include generation when you need to choose the item type, item total, or guidance. |
generation.itemType | Use trivia or poll for single-type generation. Required unless generation.itemPlan is used. |
generation.itemCount | Optional integer. Anti-abuse usage limits apply. Total items for this request, not per topic. Omitted inside generation defaults to 4. |
generation.guidance | Optional editorial instructions, up to 500 characters. Omit the key, or send a non-empty string — blank or whitespace-only values return 400 invalid_request. Examples: "Focus on military history", "Keep answers under five words", "Suitable for 8th graders". |
Unsupported generation fields | Unsupported generation keys return 400 invalid_request. |
Idempotency fingerprint | Idempotency replays include generation only when the client sends a generation key. Omitting generation does not fingerprint default item controls. |
publication.mode | Optional publication behavior. manual, the default, leaves the generated edition unpublished. publishWhenReady publishes it after generation succeeds. |
normalizingqueuedrunningsucceededfailedcancelledRetry-After when present on job polls and on request-rate 429 responses. Do not use a fixed interval.Retry-After | Job polls may include Retry-After (typically 2 seconds while normalizing, queued, or running). Honor it instead of a fixed interval. Request-rate 429 responses also include Retry-After. |
|---|---|
X-RateLimit-* | Every authenticated response may include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for the partner HTTP budget. |
Async failures | If a job reaches status failed, GET /v1/generation-jobs/:id includes error.code, error.message, and error.retryable. Retry with a new Idempotency-Key when retryable is true. |
AI quota | POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate may return 429 ai_quota_exceeded with X-AI-Quota-Status: exhausted (no Retry-After). See Errors and Rate limits. |
Synchronous failures | Auth, validation, idempotency, rate-limit, and budget admission failures return the standard error envelope and may not create a Generation Job. |
|---|---|
Async failures | If a job reaches status failed, GET /v1/generation-jobs/:id includes error.code, error.message, and error.retryable. |
Retry policy | When error.retryable is true, retry by sending a new generate request with a new Idempotency-Key. |
Completion callbacks | Webhooks are not available in v1. Poll GET /v1/generation-jobs/:id and honor Retry-After. |
|---|---|
High-volume polling | Run polling from a backend worker, not page-render code. Avoid polling an active job more often than every 2 seconds. |
async function generateQuizAndPoll(
apiBaseUrl,
apiKey,
maxPollAttempts = 60
) {
const pollAttemptLimit =
Number.isInteger(maxPollAttempts) && maxPollAttempts > 0
? Math.min(maxPollAttempts, 60)
: 60;
async function readApiError(response, fallbackMessage) {
let body = null;
try {
body = await response.json();
} catch {
// A non-JSON error still uses the public HTTP fallback below.
}
const code = body?.error?.code;
const message = body?.error?.message ?? fallbackMessage;
const requestId = body?.error?.requestId ?? response.headers.get('X-Request-Id');
return {
code,
limit: body?.limit,
message: requestId ? `${message} (requestId: ${requestId})` : message,
};
}
function parseRetryAfterMs(value) {
const normalized = value?.trim();
if (!normalized) return null;
if (/^\d+$/.test(normalized)) {
const seconds = Number(normalized);
return Number.isSafeInteger(seconds) ? seconds * 1000 : null;
}
if (/^[+-]?\d/.test(normalized)) return null;
const retryAt = Date.parse(normalized);
return Number.isNaN(retryAt) ? null : Math.max(retryAt - Date.now(), 0);
}
function handleJobSnapshot(job) {
switch (job.status) {
case 'normalizing':
case 'queued':
case 'running':
return null;
case 'succeeded':
if (job.publication?.state !== 'live') {
// Publish first, then use embed fields from the publish response.
console.log('Publish before embedding:', job.quizId, job.editionId);
} else {
console.log(job.embed.iframeHtml);
}
return job;
case 'failed':
case 'cancelled': {
const reason = job.error
? `${job.error.code}: ${job.error.message}`
: `status ${job.status}`;
const retryGuidance = job.error?.retryable
? ' Retry with a new Idempotency-Key.'
: ' Do not retry this failure.';
throw new Error(`Generation Job did not succeed: ${reason}.${retryGuidance}`);
}
default:
throw new Error(`Unknown Generation Job status: ${String(job.status)}`);
}
}
// POST /v1/quizzes/generate is async — it returns immediately with a job id.
const startResponse = await fetch(`${apiBaseUrl}/v1/quizzes/generate`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(), // required; reuse only for the same request body
},
body: JSON.stringify({
input: 'The Roman Empire',
generation: { itemType: 'trivia', itemCount: 4 },
}),
});
if (!startResponse.ok) {
const error = await readApiError(
startResponse,
`Generation request failed (${startResponse.status})`
);
throw new Error(error.message);
}
const initialJob = await startResponse.json();
const { jobId } = initialJob;
if (!jobId) throw new Error('Generation response did not include a jobId.');
const initialResult = handleJobSnapshot(initialJob);
if (initialResult) return initialResult;
const location = startResponse.headers.get('Location') ?? `/v1/generation-jobs/${jobId}`;
const jobUrl = location.startsWith('http')
? location
: `${apiBaseUrl}${location.startsWith('/') ? '' : '/'}${location}`;
// Poll GET /v1/generation-jobs/:id until the job leaves the active states.
for (let attempt = 0; attempt < pollAttemptLimit; attempt += 1) {
const response = await fetch(jobUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
const error = await readApiError(
response,
`Generation Job request failed (${response.status})`
);
const retryAfterMs = parseRetryAfterMs(response.headers.get('Retry-After'));
if (
response.status === 429 &&
error.code === 'rate_limited' &&
!error.limit &&
retryAfterMs !== null
) {
await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
continue;
}
throw new Error(error.message);
}
const job = await response.json();
const result = handleJobSnapshot(job);
if (result) return result;
const retryAfterMs = parseRetryAfterMs(response.headers.get('Retry-After')) ?? 2000;
await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
}
throw new Error('Generation Job timed out while polling.');
}Make an edition live
Manual publish (default) | Omit publication or set publication.mode to manual. Publish after the job succeeds. |
|---|---|
Publish when ready | Set publication.mode to publishWhenReady. The API publishes the generated edition after generation succeeds. Poll until publication.state is live, or publish manually if state is publish_failed. |
Publish a specific edition | POST /v1/quizzes/:id/publish publishes the current unpublished edition. POST /v1/quizzes/:id/editions/:editionId/publish publishes a particular edition — use the editionId from the generate or job response when you generated multiple editions. |
Publishing changes what the quiz’s stable URL and embed show.
POST /v1/quizzes/:id/publish publishes the quiz’s current unpublished edition. Use POST /v1/quizzes/:id/editions/:editionId/publish to publish a specific generated edition.
curl https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes/6ba7b810-9dad-11d1-80b4-00c04fd430c8/publish \
-H "Authorization: Bearer atriv_live_REPLACE_ME" \
-H "Content-Type: application/json" \
-d '{}'curl -X POST https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/quizzes/6ba7b810-9dad-11d1-80b4-00c04fd430c8/editions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/publish \
-H "Authorization: Bearer atriv_live_REPLACE_ME" \
-H "Content-Type: application/json" \
-d '{}'{
"input": "The Roman Empire",
"generation": {
"itemType": "trivia",
"itemCount": 4
},
"publication": {
"mode": "publishWhenReady"
}
}Single envelope on every 4xx/5xx
{
"error": {
"code": "invalid_request",
"message": "Request input is invalid.",
"requestId": "f4c8ab12-3e67-4d91-8b25-7a0c6e9d143f"
}
}invalid_api_key | Missing, revoked, or wrong Partner key. |
|---|---|
invalid_request | Request input or resource state is invalid. Publishing an empty or otherwise non-publishable edition returns this code. |
idempotency_key_required | POST /v1/quizzes/generate, POST /v1/quizzes/:id/generate, and POST /v1/email-image-sets require Idempotency-Key. |
quiz_not_found | Requested Quiz is missing or not visible. Embed-slug reads also use this code for unpublished Quizzes. |
email_image_set_not_found | Email image set id not visible to this account. |
generation_job_not_found | Generation Job id not visible to this account. |
domain_not_approved | Domain is not approved for this account in the Portal. |
idempotency_conflict | Same Idempotency-Key, different body. Reuse body or pick a new key. |
idempotency_in_progress | Same Idempotency-Key request is still running. Retry after the original completes. |
feature_not_enabled | Partner feature flag off for the requested capability. |
plan_limit_exceeded | Requested operation exceeds the Partner plan limit. The top-level limit field contains scope, limit, remaining, and resetsAt. |
render_failed | Email image rendering failed before a ready image set existed. Honor Retry-After before retrying. |
ai_quota_exceeded | Generation capacity used for the current period on POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate; response includes X-AI-Quota-Status: exhausted (no Retry-After). |
rate_limited | Back off using Retry-After and X-RateLimit-* headers. Only generation-capacity blocks also include top-level limit metadata. |
invalid_items | Selected trivia or poll item content is not publishable. |
edition_conflict | Cannot PATCH a live (published) edition. |
X-AI-Quota-Status | Value exhausted. Only on 429 responses with error.code ai_quota_exceeded from POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate. |
|---|
requestId when contacting support.{
"error": {
"code": "quiz_not_found",
"message": "No Quiz exists with id '6ba7b810-9dad-11d1-80b4-00c04fd430c8' for this account.",
"requestId": "f4c8ab12-3e67-4d91-8b25-7a0c6e9d143f",
"details": {
"quiz_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
}
}
}Per-key and partner HTTP request budgets. Request-rate 429 responses include Retry-After. Generation-capacity exhaustion on POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate returns 429 ai_quota_exceeded with X-AI-Quota-Status: exhausted (no Retry-After, no usage counts).
Items per job (Enterprise) | 40 |
|---|---|
HTTP requests per key | 60/min Starter or Standard; 300/min Enterprise |
HTTP requests per partner | 120/min Individual; 500/min Business; 1,000/min Enterprise |
X-RateLimit-Limit | Authoritative request limit for the current window. |
|---|---|
X-RateLimit-Remaining | Requests left in the window. |
X-RateLimit-Reset | Unix time (seconds) when the window resets. |
429 for request-rate limits includes Retry-After and fresh X-RateLimit-Reset. Wait before retrying polls or writes.
X-AI-Quota-Status | Value exhausted. Only on 429 responses with error.code ai_quota_exceeded from POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate. |
|---|
When the partner's generation capacity is used for the current period, POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate return 429 with error.code: ai_quota_exceeded. That response does not include Retry-After.
No job admitted | Validation, authentication, idempotency conflict, request-rate, and immediate generation-capacity failures do not create a Generation Job. |
|---|---|
Job started | Once model generation begins, AI usage may count even if the async Generation Job later fails. |
Budget exhausted | POST /v1/quizzes/generate and POST /v1/quizzes/:id/generate return 429 ai_quota_exceeded with X-AI-Quota-Status: exhausted. |
Schemas and the full v1 contract
Machine-readable OpenAPI 3. Use for SDK generation, contract tests, or importing into Postman. This page is derived from the same document; if anything disagrees, trust the JSON.
https://gtlwpwczbqfepuxphjyr.supabase.co/functions/v1/api/v1/openapi.json