Merge flashcard decks
Combine two flashcard decks without repeated prompts: a deck with photosynthesis and mitosis plus one with mitosis and meiosis becomes 3 cards, and the first mitosis card keeps its answer. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Study apps, LMS exports, and Anki-style CSV dumps often leave you with two overlapping decks that share the same prompt on many cards. Pasting them by hand invites silent duplicates that waste review time and skew spaced-repetition scheduling. This flashcard deck merge tool accepts two decks as arrays of front and back pairs, walks deck A then deck B, and returns a single ordered list where each exact front appears only once—the first occurrence wins and keeps its original back.
How the merge and front-only dedupe work
A flashcard here is a simple pair: a front string (the prompt or question) and a back string (the answer or definition). For every card it asks whether the exact front string has already been kept; if not, the card is appended with its back unchanged, and the front is recorded as seen. Exact means character-for-character equality: leading or trailing spaces, different capitalisation, or different punctuation create distinct fronts, which is intentional so that near-misses remain visible for human review instead of being silently collapsed.
When the same front appears again later—whether inside the same deck or in the second deck—the later card is skipped entirely, including a different back. Counts in the response report how many cards each input had, how many survived, and how many were removed as front duplicates, so you can log the delta without re-scanning the arrays yourself.
What you can do with it
Anki and LMS export cleanup
Union two vocabulary or quiz exports and drop exact duplicate fronts before re-importing into spaced-repetition software.
Nightly curriculum pack reconcile
Automate merges of partner study packs in ETL with deterministic counts and no billing on empty-deck validation errors.
FAQ
What happens if either deck is empty?
Both decks must contain at least one card so empty merges are never treated as success and are not billed.
Is duplicate detection case-sensitive?
Yes. Fronts are compared with exact string equality, so different capitalisation or spacing counts as a different card and is kept.
Which back is kept when the same front appears twice?
Later cards with that front are dropped entirely.
Are duplicates inside the same deck removed too?
Yes. Any later card whose front has already been kept is skipped, whether the earlier copy sits in the first deck or earlier in the second one. The counts show how many were removed.
Which deck should I put first?
The one with the wording you trust, such as your own reviewed deck, with a bulk import second. The first occurrence of each front wins, so the first deck decides which answer survives.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/edu/flashcard-deck-merge \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"deck_a":[{"front":"What is photosynthesis?","back":"Plants turning light into chemical energy."},{"front":"What is mitosis?","back":"Division into two identical cells."}],"deck_b":[{"front":"What is mitosis?","back":"Duplicate front: dropped, deck_a wins."},{"front":"What is meiosis?","back":"Division into four distinct gametes."}]}'const res = await fetch("https://api.kit.forhosting.com/edu/flashcard-deck-merge", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"deck_a": [
{
"front": "What is photosynthesis?",
"back": "Plants turning light into chemical energy."
},
{
"front": "What is mitosis?",
"back": "Division into two identical cells."
}
],
"deck_b": [
{
"front": "What is mitosis?",
"back": "Duplicate front: dropped, deck_a wins."
},
{
"front": "What is meiosis?",
"back": "Division into four distinct gametes."
}
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/edu/flashcard-deck-merge",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"deck_a": [
{
"front": "What is photosynthesis?",
"back": "Plants turning light into chemical energy."
},
{
"front": "What is mitosis?",
"back": "Division into two identical cells."
}
],
"deck_b": [
{
"front": "What is mitosis?",
"back": "Duplicate front: dropped, deck_a wins."
},
{
"front": "What is meiosis?",
"back": "Division into four distinct gametes."
}
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/edu/flashcard-deck-merge", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"deck_a":[{"front":"What is photosynthesis?","back":"Plants turning light into chemical energy."},{"front":"What is mitosis?","back":"Division into two identical cells."}],"deck_b":[{"front":"What is mitosis?","back":"Duplicate front: dropped, deck_a wins."},{"front":"What is meiosis?","back":"Division into four distinct gametes."}]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"deck_a":[{"front":"What is photosynthesis?","back":"Plants turning light into chemical energy."},{"front":"What is mitosis?","back":"Division into two identical cells."}],"deck_b":[{"front":"What is mitosis?","back":"Duplicate front: dropped, deck_a wins."},{"front":"What is meiosis?","back":"Division into four distinct gametes."}]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/edu/flashcard-deck-merge", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"deck_a": [
{
"front": "What is photosynthesis?",
"back": "Plants turning light into chemical energy."
},
{
"front": "What is mitosis?",
"back": "Division into two identical cells."
}
],
"deck_b": [
{
"front": "What is mitosis?",
"back": "Duplicate front: dropped, deck_a wins."
},
{
"front": "What is meiosis?",
"back": "Division into four distinct gametes."
}
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "edu.flashcard_deck_merge",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
Either empty deck is rejected with invalid_input so automation never pretends an empty merge succeeded.
How the merge and front-only dedupe work
The merger concatenates deck_a in order, then deck_b, and builds the result with a single pass over that combined sequence. That first-wins rule matches how most study exports already order preferred wording: put the authoritative deck first as deck_a and the bulk import second as deck_b. The algorithm is pure and deterministic: identical inputs always produce identical JSON, with no random seeds, clocks, or external services.
Input contract, response shape, and errors
POST a JSON body with two required arrays, deck_a and deck_b. Each array must contain at least one object with string fields front and back (numeric or boolean sides are coerced to decimal or true/false text; nested objects are rejected). Empty arrays, missing decks, non-array values, and cards without both sides fail with invalid_input naming the problem, and those failures are not billed. On success the body includes merged, the ordered list of surviving {front, back} cards; count, the length of merged; deck_a_count and deck_b_count for the validated inputs; total_input as their sum; duplicates_removed as total_input minus count; and operation set to flashcard_deck_merge for easy routing in multi-tool pipelines. Example: deck_a with fronts photosynthesis and mitosis, and deck_b with a second mitosis plus meiosis, yields three cards—photosynthesis, mitosis from deck_a, and meiosis—because the duplicate mitosis front from deck_b is dropped while its newer back is ignored. Pair the result with your own CSV or Anki writer if you need a file on disk; this endpoint returns structured cards only and does not emit binary packages or media attachments.
Where merging study decks earns its keep
Language learners exporting two Anki decks for the same vocabulary list can drop exact duplicate lemmas without opening a desktop GUI. Because validation failures do not consume balance, you can probe empty imports safely during ETL. Teachers who maintain a master biology deck and a class-shared extension often need a clean union before publishing to an LMS: put the master as deck_a so preferred backs stick when students rephrased the same question.
Integration use cases
Teacher master plus class extension
Place the authoritative curriculum deck in deck_a and a class-contributed deck in deck_b so preferred backs win when fronts collide.
Technical questions
How do I merge two flashcard decks without duplicate fronts?
Send non-empty arrays deck_a and deck_b of {front, back} cards. The API walks deck_a then deck_b and keeps only the first exact front, returning merged and duplicates_removed.
What does the flashcard deck merge tool cost?
The browser tool is free. API requests use prepaid KIT balance at $0.002 per successful call; invalid input is not billed.