@quizbase/client
v0.8.0
Published
TypeScript SDK for QuizBase API — typed client, retry, RFC 9457 errors, telemetry hook. Generated from openapi.json.
Maintainers
Readme
@quizbase/client
TypeScript SDK for the QuizBase API — a multilingual trivia API. 1.4M+ blended, deduped questions from 11 open datasets (CC, MIT), English and Polish at launch. Per-record license, author, and source on every response.
0.x — API may change. Generated from the live OpenAPI 3.1 spec, but the wrapper ergonomics may shift before 1.0. We aim to lock the surface ~6–8 weeks after public launch based on user feedback.
Install
pnpm add @quizbase/client
# or: npm install @quizbase/client
# or: yarn add @quizbase/clientRequires Node ≥20 (or any modern browser with fetch + AbortController).
Quick start
import { createClient } from '@quizbase/client';
const client = createClient({
apiKey: process.env.QUIZBASE_API_KEY! // qb_pk_… (publishable) or qb_sk_… (secret)
});
const random = await client.questions.random({ category: 'science-and-nature', lang: 'en' });
console.log(random.data[0].text);Get a key at quizbase.runriva.com/dashboard/keys. The free tier is 500 requests/day across all your keys against full production data — enough to ship a real app. Paid tiers at /pricing.
Use qb_pk_* (publishable) from browsers and edge functions — CORS is enabled. Use qb_sk_* (secret) only from servers and CI; CORS is blocked for these. Both formats authenticate identically and share the same per-user quota.
Resources
client.questions.list({ category, tags_any, topics_any, subcategory, lang, cursor, limit, ... });
client.questions.random({ category, tags, lang, ... });
client.questions.get(id, { lang });
client.categories.list({ lang });
client.languages.list();
client.topics.list({ lang });
client.topics.get(slug, { lang });
client.tags.list({ lang });
client.subcategories.list({ lang });
client.regions.list({ lang, kind, q });
client.stats.get();
client.me.get();
client.usage.get({ from, to });
client.report.create({ questionId, kind, comment });Full parameter docs: docs.quizbase.runriva.com/docs and the interactive API Reference.
Tip: every question has a stable UUID
idthat never changes. Save it client-side and compose it into multi-language quizzes, daily challenges, anti-cheat, and more — see Client patterns below.
Pagination
questions, topics, tags, subcategories, and regions are cursor-paginated. Use listAll() to iterate every item, or pages() to iterate page-by-page — both auto-follow _links.next and preserve filters across pages.
// Iterate every matching question. Filters and limit are reused on each page.
for await (const q of client.questions.listAll({ lang: 'pl', tags_any: ['einstein'] })) {
await db.upsert(q);
}
// Iterate pages — useful when you need batch boundaries (checkpoints, logging).
for await (const page of client.topics.pages({ lang: 'en' })) {
console.log(page.meta.requestId, page.data.length);
}Need manual control? list() still exposes _links.next and a cursor query param.
Client patterns with stable IDs
Every question carries a stable UUID v7 id that never changes after import. The SDK is intentionally thin — no ?seed=, no ?daily=, no built-in deduplication. The stable id is the primitive you compose into mechanics. Six patterns cover most real apps:
Same question across languages
const en = await client.questions.random({ lang: 'en' });
const id = en.data[0].id;
const pl = await client.questions.get(id, { lang: 'pl' });
const es = await client.questions.get(id, { lang: 'es' });
// Same canonical question, three languages — UUID is the link.Don't repeat — exclude seen questions
const seen = new Set<string>();
const batch = await client.questions.random({
category: 'science-and-nature',
amount: 20,
exclude: [...seen]
});
batch.data.forEach((q) => seen.add(q.id));exclude accepts up to 250 UUIDs. For longer histories keep seen client-side and filter after the fetch.
Daily challenge — same question for everyone today
const day = new Date().toISOString().slice(0, 10); // "2026-05-19"
const idx = [...day].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7);
const pool = await client.questions.list({ category: 'history', limit: 50 });
const todays = pool.data[Math.abs(idx) % pool.data.length];
// Persist `todays.id` server-side so retries serve the same question.Multiplayer sync — both players see the same question
Your matchmaker picks one id (via random or list), broadcasts to both clients, and both clients call questions.get(id, { lang }). Same UUID, per-player language preference. Zero state on QuizBase — your matchmaker owns the round.
Server-side anti-cheat — never ship correctAnswer to the client
Use qb_sk_* (secret key) from your server to fetch full questions including correctAnswer. Forward only id + text + a shuffled [correctAnswer, ...incorrectAnswers] to the client. Validate the submitted answer server-side by re-fetching with the same id. Browsers cannot reach qb_sk_* — CORS blocks it.
Stable Anki / Quizlet flashcards across updates
Save {id, lang} as your card key. When the upstream question changes (typo fix, distractor swap, translation refresh), questions.get(id, { lang }) returns the updated content under the same id — your card auto-updates without re-import. Soft-deleted questions return 404; treat that as "card removed upstream".
Full code samples and edge cases at /docs/api/questions-by-id § What you can do with a stable id. For MCP agents, the same playbook is exposed as the client_mechanics_patterns prompt.
Regions: cultural affinity, not geography
The regions field marks cultural affinity — residents of a country or members of a cultural/religious group are statistically more likely to know the answer. It is not a tag for "this question is about country X". The Mona Lisa is universally accessible (regions: []), but a question requiring NFL knowledge has regions: ["us"].
Values:
- Country codes — ISO 3166-1 alpha-2 lowercase (
us,pl,gb,de,jp, …) - Cultural codes —
jewish,christian-catholic,islam
// US-relevant questions (NFL, US presidents, Super Bowl)
const { data } = await client.questions.random({ regions: ['us'], amount: 5 });
// Catholic doctrine
const { data } = await client.questions.random({ regions: ['christian-catholic'] });
// AND-logic: both Polish AND Catholic
const { data } = await client.questions.random({
regions: ['pl', 'christian-catholic']
});
// Discover the full catalog (~150 codes per language)
const { data: regions } = await client.regions.list({ lang: 'en', kind: 'cultural' });
// [
// { code: 'jewish', kind: 'cultural', label: 'Jewish (cultural/religious)', count: 2698 },
// { code: 'christian-catholic', kind: 'cultural', label: 'Catholic Christian (cultural/religious)', count: 2859 },
// { code: 'islam', kind: 'cultural', label: 'Islamic (cultural/religious)', count: 784 }
// ]
// Native labels for `lang=pl`: pl → "Polska", jp → "日本"
for await (const region of client.regions.listAll({ lang: 'pl' })) {
console.log(region.code, region.label, region.count);
}Input is case-insensitive ('PL' and 'pl' both work — normalized server-side). Output is always lowercase. See the regions docs for the full catalog with counts.
Errors
Every non-2xx response throws QuizbaseError carrying the parsed RFC 9457 Problem Details:
import { createClient, QuizbaseError } from '@quizbase/client';
try {
await client.questions.random({ category: 'unknown' });
} catch (err) {
if (err instanceof QuizbaseError) {
console.error(err.status); // 400
console.error(err.problem.code); // "invalid_query_param"
console.error(err.problem.detail); // human-readable message
console.error(err.requestId); // for support requests
if (err.isRateLimited) console.error(err.retryAfter); // seconds
}
}Retries
By default, the SDK retries 2× (3 attempts total) on 429 and 5xx responses with exponential backoff + jitter. Server-issued Retry-After is honored. 4xx (other than 429) is not retried.
createClient({ apiKey, retries: 5 }); // tune
createClient({ apiKey, retries: 0 }); // disablePerformance-aware timeouts
Per-endpoint defaults are tuned against the public performance baseline:
| Endpoint | Default timeout |
|-------------------------------------------|-----------------|
| questions.list, topics.list, tags.list, subcategories.list, report.create | 15 s |
| questions.random, questions.get, categories.list, languages.list, topics.get, stats.get, me.get, usage.get | 10 s |
| Global default | 30 s |
Override per-endpoint:
createClient({
apiKey,
timeout: 30_000,
timeouts: {
'questions.random': 5_000 // narrow filters → fail fast
}
});Telemetry hook
Wire every HTTP attempt (including retries) to your observability stack:
import { posthog } from 'posthog-js';
const client = createClient({
apiKey,
onRequest: ({ method, endpoint, duration, status, requestId, retryCount, final, error }) => {
posthog.capture('quizbase_api_call', {
method, endpoint, duration, status, requestId, retryCount, final,
error: error?.message
});
}
});final: false means another retry will follow. The hook is async-safe; thrown errors inside it are swallowed so telemetry can never break the caller.
Custom fetch
Pass a fetch implementation for testing or to use undici/node-fetch:
createClient({ apiKey, fetch: customFetch });Type-safe responses
All responses are typed from the OpenAPI spec. Hover over client.questions.random(...) in your IDE for autocomplete on every parameter and field.
Source & releases
- GitHub: maciejdzierzek/quizbase-sdk-ts
- Issues / feedback: GitHub Issues
- Releases: automated by release-please from conventional commits
- Drift guard: every release is verified against the live OpenAPI spec at quizbase.runriva.com/openapi.json — types reflect the current API surface.
License
MIT © Maciej Dzierżek
