GET /v1/questions#
/api/v1/questions API key required Paginated browse of the full question catalog. Use this when you need all matching questions (e.g. building a local cache, a study app, or a data exporter) — not random samples.
Cursor-based pagination using UUID v7 ordering — stable across inserts, no page-number drift.
Parameters#
| Parameter | Type | Default | Description |
|---|---|---|---|
| cursor | UUID v7 | — | Last `id` from the previous page. Omit for the first page. |
| limit | integer | 20 | Page size, max 100. |
| lang | enum | en | Supported: `en`, `pl`. Strict — any other value returns `400`. |
| updated_since | ISO 8601 | — | Only questions with `updatedAt >= this`. Delta-sync pattern. |
| category | integer or slug | — | Category id (1-24 internal, or 9-32 opentdbId) or slug. |
| difficulty | enum | — | `trivial` | `easy` | `medium` | `hard` | `expert` — LLM-calibrated 5-level difficulty graded with distractor-set context. `trivial` = common-knowledge (>80% of adults), `easy` = approachable, `medium` = requires-thought, `hard` = needs-domain-context, `expert` = specialist (<10%). Pre-rescore records hold an importer placeholder (mostly `medium` for factoid sources). |
| type | enum | — | Default response includes `multiple` and `boolean` only. Pass `?type=text_input` to opt into open-ended questions explicitly. |
| tags | CSV kebab-case | — | All-of match (AND). Each returned question echoes tags as `{slug, label}` objects. |
| tags_any | CSV kebab-case | — | Any-of match (OR), max 10. Use when AND logic is too restrictive. |
| topic | kebab-case | — | Curated topic slug (resolves through aliases). 2,184 curated topics — see [GET /v1/topics](/docs/api/topics). |
| topics_any | CSV kebab-case | — | Any-of match (OR) on curated topics, max 10. |
| subcategory | kebab-case | — | Raw subcategory slug. Single-value filter; prefer `topic`/`topics_any` for OR semantics across many. |
| quality | enum | high | Three tiers, nested `high` ⊆ `standard` ⊆ `all`. `high` (default) returns only the cleanest, most broadly-useful questions. `standard` broadens to the full reviewed pool including niche/narrowly-scoped questions (more volume). `all` includes everything, even questions flagged for review — useful for audit, research, BY-SA dump. When `quality=all` each returned question gains a `"quality": "high" | "needs_review"` field so you can tell which records were flagged; `high`/`standard` skip the field. |
| regions | CSV lowercase ISO + cultural | — | **Cultural affinity** — residents of the country or members of the cultural/religious group are statistically more likely to know the answer (NOT geography of the subject). Lowercase ISO 3166-1 alpha-2 (`pl`, `us`, `gb`) plus cultural codes (`jewish`, `christian-catholic`, `islam`). AND-logic. Empty array on a question = universally accessible (no cultural advantage). Uppercase tolerated (normalized server-side). Discover the full catalog via [GET /v1/regions](/docs/api/regions). |
| source | CSV source ids | — | Include only these sources. One or more of: `arc`, `creak`, `entityq`, `kqa-pro`, `mintaka`, `mkqa`, `nq-open`, `opentdb`, `opentriviaqa`, `qasc`, `quizbase`, `webq`. Comma-separated for multiple (OR), e.g. `source=mintaka,opentdb`. |
| exclude_source | CSV source ids | — | Exclude these sources, e.g. `exclude_source=entityq` to drop a noisy auto-generated source. Comma-separated for multiple. Applied after `source` if both are present. |
| license | SPDX | — | e.g. `CC-BY-SA-4.0`, `MIT`. Filter by license string. |
| count | enum | none | Total strategy: `none` (default — page via cursor/`_links.next`, no total); `exact` (precise `COUNT(*)`, ~25-90ms) → `meta.total`. |
| ids | CSV UUID | — | Batch fetch — up to 250 comma-separated ids in one call. Returns those exact records (anti-repeat, deep-links, restoring a saved set), no quality filter. Terminal selector: browse filters and `cursor` are ignored. Missing ids are reported in `meta.missing`. |
| content_language | enum | — | Language of the returned question **text** (`en`, `pl`). With `ids`, maps each id to its sibling in this language across the translation chain — the same questions in another language. Distinct from `lang` (display labels only). |
Examples#
First page#
Delta sync#
Fetch only questions updated since your last sync:
curl -H "X-API-Key: qb_pk_YOUR_KEY"
"https://quizbase.runriva.com/api/v1/questions?lang=pl&updated_since=2026-04-01T00:00:00Z&limit=100" Persist the highest updatedAt you’ve seen and use it as the next updated_since.
Batch by id + translation mapping#
Fetch a known set in one call, or map it to another language across the translation chain. Ids without a result come back in meta.missing; results are ordered to match the ids you sent.
Pass the ids you hold — the server resolves the translation family (originals are the root of their chain, so their rootQuestionId is null; you don’t compute roots). Full walkthrough: Languages and translations.
Response#
{
"data": [
{
"id": "0193f8b5-7e5c-7c24-9f7a-3d1e1c2a5f10",
"text": "Jaka jest stolica Polski?",
"correctAnswer": "Warszawa",
"incorrectAnswers": ["Kraków", "Gdańsk", "Wrocław"],
"type": "multiple",
"difficulty": "easy",
"language": "pl",
"category": { "id": 14, "slug": "geography", "name": "Geografia" },
"subcategories": [
{ "slug": "european-capitals", "label": "Stolice europejskie" },
{ "slug": "central-europe", "label": "Europa Środkowa" }
],
"tags": [
{ "slug": "capitals", "label": "Stolice" },
{ "slug": "europe", "label": "Europa" }
],
"regions": ["pl"],
"attribution": {
"author": "community",
"source": "opentriviaqa",
"license": "CC-BY-SA-4.0",
"licenseVersion": "4.0",
"licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0/",
"sourceId": "otq:12345",
"url": "https://opentriviaqa.example/q/12345",
"modifications": ["translated_pl"],
"lastModified": "2026-04-24T10:00:00Z"
},
"translationOf": "0193f8b5-7e5c-7c24-9f7a-000000000001",
"rootQuestionId": "0193f8b5-7e5c-7c24-9f7a-000000000001",
"translator": "machine",
"explanation": null,
"extensions": {
"subcategories": ["european-capitals", "central-europe"]
},
"createdAt": "2026-04-24T10:00:00Z",
"updatedAt": "2026-04-24T10:00:00Z"
}
],
"meta": { "count": 50, "countMode": "none", "language": "pl", "requestId": "req_..." },
"_links": {
"next": "/api/v1/questions?lang=pl&cursor=019db5a1...&limit=50"
}
}{
"type": "https://quizbase.runriva.com/errors/invalid_query_param",
"title": "Invalid query parameters",
"status": 400,
"detail": "cursor: Invalid uuid",
"instance": "/api/v1/questions?cursor=not-a-uuid",
"code": "invalid_query_param",
"errors": [
{ "path": "cursor", "message": "Invalid uuid" }
]
}Response fields#
tags/subcategories— both arrays of{ slug, label }. Theslugis the stable identifier you filter by; thelabelis a human-readable string in the requested?lang=. Tags are short identifiers (≤4 words, proper nouns and concepts), subcategories are broader topic groupings (≤4 words, organic). A question typically has 3-5 tags and 3-6 subcategories.category— the top-level category (24 in total).idis the legacy OpenTDB id (9-32).nameis localized to?lang=with English fallback if a translation is missing.translationOf/rootQuestionId— non-null when this row is a translation.translationOfis the direct parent (usually English source),rootQuestionIdis the canonical source across translation chains.translator— coarse provenance:"machine"(machine-translated from the source language),"human"(manually translated/edited),"native"(imported as-is from a multilingual upstream source), ornull(original-language record, no translation involved). The specific upstream system that produced a machine translation is an implementation detail and not exposed.extensions— forward-compatible container for stable per-source extras. Today exposessubcategories: string[](also surfaced as the typed top-levelsubcategories: [{slug, label}]). Future stable fields land here additively. Internal pipeline observability (model versions, prompt versions, run IDs) is not exposed here — that’s audit data we keep server-side.
License metadata in attribution#
The 9-field attribution object gives you everything needed to comply with upstream licenses without consulting external docs:
license— the effective applied license string (e.g.CC-BY-SA-4.0— with3.0 → 4.0upgrade for sources where applicable).licenseVersion— the original upstream license version ("3.0","4.0", ornullfor MIT). Use this when you need the exact version the upstream record came under.licenseUrl— the canonical license URL (creativecommons.org or opensource.org).nullfor proprietary records.modifications— array of changes we made on top of the upstream record. Possible values:translated_<lang>(per-language),refined_text(we improved phrasing),quizified(we converted a Q&A pair into a quiz item with distractors). An empty array means we kept the upstream record verbatim.lastModified— ISO timestamp of our last modification (updatedAt). Use this for cache invalidation andupdated_sincesyncs.
If you redistribute records covered by share-alike licenses, build your attribution string using author + licenseUrl + the modifications list.
Difficulty grading#
difficulty is graded by an LLM with the distractor set in scope — not the question alone. A boolean “is the sky blue?” rated against three nonsense distractors is trivial; the same question with three plausible-sounding distractors is medium. This is why the same factual content can land on different levels across the catalog.
Five levels:
trivial— common knowledge. Primary-school / mainstream culture. More than 80% of adults answer correctly.easy— approachable. Familiar but takes a second of thought.medium— requires thought. Most adults could get there with effort; not memorized.hard— needs domain context. Hobbyists, students, professionals in the field.expert— specialist trivia. Fewer than 10% answer correctly without research.
Pre-rescore records hold an importer-assigned placeholder (mostly medium for factoid sources like NQ, Mintaka, EntityQ). Use ?quality=high together with ?difficulty=... to skip ungraded rows when you need calibrated levels for adaptive gameplay.
Pair with ?quality=high (which keeps only records that passed distractor validation and content grading) for adaptive difficulty pipelines where the level needs to mean something.
Stop condition#
When _links.next is absent, you’ve reached the end. Some pages may return fewer than limit rows even mid-stream — do not use “fewer rows than limit” as a stop condition.
Rate limits#
Pagination lets you burn through your quota fast. Consider:
- Requesting
limit=100(max) to halve request count - Using
updated_sincefor incremental syncs after initial fetch - Caching on your side — questions don’t change often
Performance#
- p50 (warm): ~57ms
- p95: ~91ms (sustained 50 RPS, 5-min baseline)
- p99: ~118ms
- Last measured: 2026-05-30
- SLO: p95 < 500ms, error rate < 1%
- Cursor pagination scales to any catalog size — performance stays flat as you walk forward.
?count=none(default) skips the total — page forward with the cursor and_links.next. Add?count=exactfor a precisemeta.total(COUNT(*), ~25-90ms). For per-category/tag/topic totals, use the discovery endpoints (/tags,/subcategories,/topics,/regions) — each item carries an exactcount.
See also#
- GET /v1/questions/random — random sample for game sessions
- GET /v1/questions/:id — deep link a single question
- Languages and translations — map a set between languages with
ids+content_language - Rate limits in practice — scheduling patterns for large syncs