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 | — | `easy` | `medium` | `hard` |
| 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 | `high` | — | Pass `quality=high` to exclude questions our internal review flagged for distractor problems. |
| regions | CSV ISO 3166-1 | — | All-of match (AND), lowercase 2-letter codes. |
| source | enum | — | `opentdb` | `opentriviaqa` | `mkqa` | `mintaka` | `nq-open` | `kqa-pro` | `entityq` | `quizbase` | … |
| license | SPDX | — | e.g. `CC-BY-SA-4.0`, `MIT`. Filter by license string. |
| count | enum | estimate | How to compute total: `estimate` (cheap, ±5-50% via planner stats) → `meta.totalEstimate`; `exact` (slow, 5-9s on full dataset) → `meta.total`; `none` (skip). |
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.
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": "estimate", "totalEstimate": 545379, "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.
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): ~97ms
- p95: ~112ms (sustained 50 RPS, baseline)
- p99: ~200ms
- Last measured: 2026-05-07
- SLO: p95 < 500ms, error rate < 1%
- Cursor pagination scales to any catalog size — performance stays flat as you walk forward.
?count=estimate(default) returns in tens of milliseconds.?count=exactwalks the full set (5-9s on the current dataset) — use sparingly, only when you genuinely need an exact total.
See also#
- GET /v1/questions/random — random sample for game sessions
- GET /v1/questions/:id — deep link a single question
- Rate limits in practice — scheduling patterns for large syncs