Generate Anki flashcards from QuizBase API in Python#
Anki is the gold-standard spaced-repetition tool for serious study. Medical students use it for board prep, language learners use it for vocabulary, anyone trying to remember things long-term ends up there eventually. The catch: building a quality deck takes hours of card-crafting. This guide skips that work — you fetch 50 vetted trivia questions from QuizBase, pipe them into genanki, and ship a .apkg file your students (or yourself) can import in one click.
The trick that makes this guide worth following: every QuizBase question has a stable UUID id. We use that id as Anki’s guid for each note. If you regenerate the deck next month with fresh questions, Anki recognises any unchanged cards by guid and preserves the student’s review history — no resetting their progress every time you refresh the pool.
What you’ll build#
A Python script that:
- Reads a QuizBase publishable key from
QUIZBASE_KEYenv var - Fetches 50 questions matching a category and language (
/api/v1/questions) - Builds an Anki deck with one note per question — front is the question text, back is the correct answer + the three incorrect answers crossed out + source attribution
- Uses
question.idas the noteguidso deck updates preserve review history - Writes
quizbase_<category>_<date>.apkgyou can email to students or drop in a shared folder
The mechanic — stable IDs as Anki guid#
Anki identifies cards by guid, not by content. When a user imports an updated deck:
- Same
guidas an existing card → Anki updates the content but keeps the review schedule (the next-review date, ease factor, lapses count) - New
guid→ Anki adds the card as fresh (interval 1 day) guidmissing from new deck → Anki keeps the card; you can opt-in to remove via “schedule” merge mode
QuizBase question IDs are UUID v7 — globally unique, never reused. Plug them into genanki.Note(guid=question_id) and refreshing your deck is non-destructive. The same stable-ID pattern powers the MCP Slack trivia bot’s per-channel dedup and the daily quiz in Claude.ai Projects. See /docs/api/questions-by-id for the full pattern catalogue.
Stack#
- Python 3.11+ — any modern version, no async needed
genanki— builds.apkg.pip install genankirequestsorhttpx— fetch from QuizBase REST- QuizBase publishable key (
qb_pk_*) — free tier, 500 req/day. One deck regenerate = 1 API call (paginated to 50/request).
Step 1 — Get a QuizBase publishable key#
Sign in to your QuizBase dashboard, open API keys, and create a qb_pk_* (publishable) key. The publishable kind is the right choice here — your script doesn’t need write access, and pk keys are safe to commit to private repos (the free tier doesn’t bill on usage, so a leaked pk caps at 500 req/day).
If you don’t have an account, sign up — free tier is no card, full production API access.
Step 2 — Install genanki and set up the project#
pip install genanki requests Two files: gen_anki.py (the script) and an .env containing QUIZBASE_KEY=qb_pk_.... For a one-shot run you can just export the env var in your shell instead.
Step 3 — Fetch questions from QuizBase#
QuizBase REST returns questions in batches; ?limit=50 is the maximum per call. Use /api/v1/questions with filters that match your audience — kids’ deck → ?category=animals&difficulty=easy, language learners → ?category=geography&lang=es:
# gen_anki.py
import os
import requests
from datetime import date
API_KEY = os.environ['QUIZBASE_KEY']
CATEGORY = 'science-and-nature' # see /docs/api/categories for full list
LANG = 'en'
def fetch_questions(limit=50):
response = requests.get(
'https://quizbase.runriva.com/api/v1/questions',
headers={'X-API-Key': API_KEY},
params={'category': CATEGORY, 'lang': LANG, 'limit': limit},
timeout=10
)
response.raise_for_status()
return response.json()['data']
questions = fetch_questions()
print(f'Fetched {len(questions)} questions') response.json()['data'] is the question array. Each entry has id (UUID v7 — your guid), text, correctAnswer, incorrectAnswers (length 3), attribution (source, author, license, licenseUrl, url).
Step 4 — Build cards with stable IDs#
genanki needs a Model (defines the front/back template) and a Deck (the container). Notes are instantiated with guid=question.id:
import genanki
# Stable deck ID — pick once and never change (this becomes the Anki internal ID).
# Use any large random integer; this one is generated from `hash('quizbase-trivia')`.
DECK_ID = 1879502345
# Stable model ID — same rule. Picking a real random number documented in your repo
# is the canonical approach.
MODEL_ID = 1607392319
model = genanki.Model(
MODEL_ID,
'QuizBase Trivia',
fields=[
{'name': 'Question'},
{'name': 'Answer'},
{'name': 'Distractors'},
{'name': 'Attribution'},
],
templates=[{
'name': 'Card 1',
'qfmt': '<div class="q">{{Question}}</div>',
'afmt': '''
<div class="q">{{Question}}</div>
<hr>
<div class="a"><b>{{Answer}}</b></div>
<div class="d">Not: {{Distractors}}</div>
<small class="attr">{{Attribution}}</small>
''',
}],
css='''
.q { font-size: 18px; margin-bottom: 12px; }
.a { color: #2e7d32; font-size: 16px; }
.d { color: #888; font-size: 13px; margin-top: 8px; }
.attr { color: #aaa; font-size: 11px; display: block; margin-top: 12px; }
'''
)
deck = genanki.Deck(DECK_ID, f'QuizBase {CATEGORY.replace("-", " ").title()}')
for q in questions:
attribution = q['attribution']
note = genanki.Note(
model=model,
guid=q['id'], # STABLE: same id next regen = same Anki card
fields=[
q['text'],
q['correctAnswer'],
' · '.join(q['incorrectAnswers']),
f"{attribution['author']} ({attribution['license']})"
]
)
deck.add_note(note) The guid=q['id'] line is the keystone — without it, every regenerate creates duplicate cards and students lose progress. With it, your deck refreshes are non-destructive.
The Attribution field renders a credit line on the back of every card. Required by QuizBase content licenses (CC BY-SA, CC BY, MIT) — see /data for full attribution rules. Without it your deck is out of license compliance.
Step 5 — Generate the .apkg file#
output = f'quizbase_{CATEGORY}_{date.today().isoformat()}.apkg'
genanki.Package(deck).write_to_file(output)
print(f'Wrote {output} ({os.path.getsize(output)} bytes)') Run it: python gen_anki.py. You get quizbase_science-and-nature_2026-05-15.apkg (~40 KB). Email it to your class, drop it in a Google Classroom assignment, or upload to AnkiWeb for shared decks.
Students import once. Next month you regenerate with fresh questions — students re-import the same file, and Anki preserves their review history per card thanks to the stable guids.
Pitfalls#
- Don’t randomise
DECK_IDper run. Anki uses it as the deck primary key. Changing the ID creates a “second deck” for users — pick once, document in code, never change. - Same for
MODEL_ID. Changing it mid-life means existing notes have no template to render. Treat both IDs as schema migrations. q['correctAnswer']vsq['choices']— QuizBase returns separate fields. There is no pre-combinedchoicesarray, you must build the choices client-side. See the response format.- Free tier rate limit. 500 req/day is roughly 500 deck regenerates per day (each is 1 paginated call for 50 questions). For an automated daily job across multiple categories you’ll likely stay free; for ten teachers running their own scripts hourly, upgrade.
What next#
- Multi-language decks — fetch with
?lang=esthen?lang=frand create one Anki note per language pair. Same stableidacross translations means Anki treats them as cloze-style alternatives if you want. - Subdeck per topic — call
GET /api/v1/topicsto discover available topic slugs, then loop over them creating one subdeck per topic in the same.apkg. - CSV path for Quizlet — same fetch logic, write CSV instead of
.apkg, paste into Quizlet’s bulk import. Same stable IDs in the CSV’s first column let you diff updates manually. - Daily-refresh cron — schedule a daily run on Replit or Cron-job.org, upload the
.apkgto AnkiWeb shared decks. Students who subscribe see new questions automatically.
Ready to ship? Grab a free publishable key (500 req/day, no credit card), copy the snippets above, install genanki, run python gen_anki.py. First deck is in your inbox before lunch.