{"openapi":"3.1.0","info":{"title":"Kipinä Platform API","version":"1.0.0","description":"Multi-user language-learning, story-authoring, translation, progression, vocabulary, access-control, and account-management API. English is the canonical source language for stories."},"servers":[{"url":"https://kipina.adame.io"}],"tags":[{"name":"Authentication"},{"name":"Account"},{"name":"Stories"},{"name":"Curriculum"},{"name":"Learning"},{"name":"Translations"},{"name":"Vocabulary"},{"name":"Story studio"},{"name":"User management"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Session token or scoped Kipinä API key."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}}}}},"LanguagePair":{"type":"object","properties":{"learningLanguage":{"type":"string","example":"fi"},"translationLanguage":{"type":"string","example":"en"}}},"SegmentCapabilities":{"type":"object","properties":{"enabled":{"type":"boolean"},"translation":{"type":"boolean"},"audio":{"type":"boolean"},"choices":{"type":"boolean"},"vocabulary":{"type":"boolean"}}},"CurriculumDomain":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"sortOrder":{"type":"integer"},"blockCount":{"type":"integer","minimum":0},"itemCount":{"type":"integer","minimum":0},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","description","sortOrder","blockCount","itemCount","updatedAt"],"additionalProperties":false},"CurriculumLevel":{"type":"object","properties":{"cefrLevel":{"type":"string","enum":["A1","A2","B1","B2","C1","C2"]},"cefrGroup":{"type":"string","enum":["Basic User","Independent User","Proficient User"]},"functions":{"type":"array","items":{"type":"string"}},"lexicalSets":{"type":"array","items":{"type":"string"}},"communicativeGoals":{"type":"array","items":{"type":"string"}},"grammarFocus":{"type":"array","items":{"type":"string"}},"textComplexity":{"type":"object","additionalProperties":true},"cefrRationale":{"type":"string"}},"required":["cefrLevel","cefrGroup","functions","lexicalSets","communicativeGoals","grammarFocus","textComplexity","cefrRationale"],"additionalProperties":false},"CurriculumBlock":{"type":"object","description":"Language-agnostic focus block on the CEFR spine. Detail responses also include `items` with language-specific content records.","properties":{"blockId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"domain":{"type":"object","additionalProperties":true},"levels":{"type":"array","items":{"$ref":"#/components/schemas/CurriculumLevel"}},"itemCount":{"type":"integer","minimum":0},"storyCount":{"type":"integer","minimum":0},"items":{"type":"array","items":{"type":"object","additionalProperties":true}},"updatedAt":{"type":"string","format":"date-time"}},"required":["blockId","name","description","domain","levels","itemCount","storyCount","updatedAt"]},"ExamProfile":{"type":"object","description":"Exam mapping over CEFR levels, domains, functions, and weighted block preferences. Content selection remains metadata-driven.","properties":{"examId":{"type":"string"},"name":{"type":"string"},"targetCefrMin":{"type":"string","enum":["A1","A2","B1","B2","C1","C2"]},"targetCefrMax":{"type":"string","enum":["A1","A2","B1","B2","C1","C2"]},"modalities":{"type":"array","items":{"type":"string","enum":["reading","listening"]}},"preferredDomains":{"type":"array","items":{"type":"string"}},"preferredFunctions":{"type":"array","items":{"type":"string"}},"mappingConfidence":{"type":"number","minimum":0,"maximum":1},"blockPreferences":{"type":"array","items":{"type":"object","additionalProperties":true}},"updatedAt":{"type":"string","format":"date-time"}},"required":["examId","name","targetCefrMin","targetCefrMax","modalities","preferredDomains","preferredFunctions","mappingConfidence","blockPreferences","updatedAt"]},"TranslationHintQuota":{"type":"object","description":"Usage in the authenticated learner's rolling 24-hour free-hint window. The window is based on absolute instants and cannot be reset by changing the account timezone. `resetsOn` is the next instant one used slot expires, or null when no free slot is currently aging. Cached and XP-paid hints do not consume the free allowance.","properties":{"allowance":{"type":"integer","const":5},"used":{"type":"integer","minimum":0,"maximum":5,"example":2},"remaining":{"type":"integer","minimum":0,"maximum":5,"example":3},"windowHours":{"type":"integer","const":24,"description":"Rolling window length."},"timezone":{"type":"string","description":"The account IANA timezone, returned for display context only; it does not move the rolling window.","example":"Europe/Helsinki"},"resetsOn":{"type":["string","null"],"format":"date-time","description":"Absolute instant when the oldest active free hint leaves the window; null when no free hint is in the window.","example":"2026-07-19T08:15:00.000Z"}},"required":["allowance","used","remaining","windowHours","timezone","resetsOn"],"additionalProperties":false},"TranslationResult":{"type":"object","properties":{"text":{"type":"string","description":"Reviewed translated text."},"sourceText":{"type":"string","description":"Submitted text after whitespace normalization."},"fromLanguage":{"type":"string"},"toLanguage":{"type":"string"},"xpCost":{"type":"integer","minimum":0},"cached":{"type":"boolean","description":"True when an account-private canonical cache entry was reused."},"balance":{"type":"integer","minimum":0},"quota":{"$ref":"#/components/schemas/TranslationHintQuota"}},"required":["text","sourceText","fromLanguage","toLanguage","xpCost","cached","balance","quota"],"additionalProperties":false},"TooltipTarget":{"type":"object","description":"For story text, send segmentId, paragraphIndex, and inclusive word indices. Paragraph targets derive the complete range server-side. For localized interface elements, send sourceText and the reviewed helperText.","properties":{"targetType":{"type":"string","enum":["element","word","range","paragraph"]},"segmentId":{"type":"string"},"paragraphIndex":{"type":"integer","minimum":0},"startIndex":{"type":"integer","minimum":0},"endIndex":{"type":"integer","minimum":0},"sourceText":{"type":"string","maxLength":500},"helperText":{"type":"string","maxLength":800},"fromLanguage":{"type":"string","example":"fi"},"toLanguage":{"type":"string","example":"en"}},"required":["targetType"]},"TooltipPurchase":{"type":"object","properties":{"id":{"type":"string"},"storyId":{"type":["string","null"]},"segmentId":{"type":["string","null"]},"targetType":{"type":"string","enum":["element","word","range","paragraph"]},"contextKey":{"type":"string"},"paragraphIndex":{"type":"integer"},"startIndex":{"type":"integer"},"endIndex":{"type":"integer"},"sourceText":{"type":"string"},"translatedText":{"type":"string"},"wordCount":{"type":"integer","minimum":1},"xpCost":{"type":"integer","minimum":0},"totalXpCost":{"type":"integer","minimum":0},"refundAmount":{"type":"integer","minimum":0},"unpaidWords":{"type":"integer","minimum":0},"purchased":{"type":"boolean"},"cached":{"type":"boolean"},"balance":{"type":"integer","minimum":0}}},"TooltipUndoResult":{"type":"object","properties":{"undone":{"type":"boolean","const":true},"id":{"type":"string"},"refundAmount":{"type":"integer","minimum":0},"balance":{"type":"integer","minimum":0}},"required":["undone","id","refundAmount","balance"],"additionalProperties":false},"SkillMastery":{"type":"object","properties":{"skill":{"type":"string","enum":["reading","comprehension","vocabulary","word_order","listening"]},"label":{"type":"string"},"score":{"type":"number","minimum":0,"maximum":100},"attempts":{"type":"integer","minimum":0},"correct":{"type":"integer","minimum":0},"accuracy":{"type":"integer","minimum":0,"maximum":100},"lastPracticedAt":{"type":["string","null"],"format":"date-time"}},"required":["skill","label","score","attempts","correct","accuracy","lastPracticedAt"],"additionalProperties":false},"LearningPlan":{"type":"object","description":"A private plan computed only from the authenticated learner's own daily usage, mastery, saved words, progress, and currently enabled published story content.","properties":{"date":{"type":"string","format":"date","description":"Current calendar date in the account timezone; daily XP progress uses this local day."},"timezone":{"type":"string","description":"IANA timezone used for the plan and quota day."},"dailyGoal":{"type":"object","properties":{"targetXp":{"type":"integer","minimum":5,"maximum":100},"earnedXp":{"type":"integer","minimum":0,"description":"XP earned during this learner-local day. XP spent is not subtracted."},"remainingXp":{"type":"integer","minimum":0},"percentage":{"type":"integer","minimum":0,"maximum":100},"complete":{"type":"boolean"}},"required":["targetXp","earnedXp","remainingXp","percentage","complete"],"additionalProperties":false},"translationHints":{"$ref":"#/components/schemas/TranslationHintQuota"},"mastery":{"type":"array","items":{"$ref":"#/components/schemas/SkillMastery"}},"dueReviews":{"type":"object","description":"Due saved words belonging to the authenticated learner. `items` is capped while `count` is the full total.","properties":{"count":{"type":"integer","minimum":0},"items":{"type":"array","maxItems":8,"items":{"type":"object","additionalProperties":true}}},"required":["count","items"]},"recommendations":{"type":"array","description":"Ranked only from published stories and segments enabled for this learner.","items":{"type":"object","additionalProperties":true}}},"required":["date","timezone","dailyGoal","translationHints","mastery","dueReviews","recommendations"],"additionalProperties":false},"AdaptiveExercise":{"type":"object","description":"A personalized exercise derived from enabled published content. Expected answers and choice correctness are deliberately omitted before submission.","properties":{"id":{"type":"string","description":"Stable deterministic exercise ID."},"type":{"type":"string","enum":["cloze","word_order","listening"]},"skill":{"type":"string","enum":["vocabulary","word_order","listening"]},"story":{"type":"object","additionalProperties":true},"segment":{"type":"object","additionalProperties":true},"language":{"type":"string"},"xpReward":{"type":"integer","minimum":0,"example":5},"prompt":{"type":"string"},"clue":{"type":"string"},"tokens":{"type":"array","items":{"type":"string"}},"audioText":{"type":"string","description":"Text intended for client-side speech synthesis."},"choices":{"type":"array","description":"Listening choices without correctness flags.","items":{"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer"},"icon":{"type":["string","null"]},"text":{"type":"string"}},"required":["id","position","text"],"additionalProperties":false}},"completed":{"type":"boolean","description":"Whether this account already completed the exercise correctly."}},"required":["id","type","skill","story","segment","language","xpReward","prompt","completed"],"additionalProperties":false},"ExerciseAnswerRequest":{"type":"object","description":"Provide at least one answer form. Use `answer` for cloze, `tokens` (or an array in `answer`) for word order, and `choiceId` for listening. If more than one is supplied, the server evaluates `answer`, then `choiceId`, then `tokens`.","properties":{"answer":{"oneOf":[{"type":"string","minLength":1,"maxLength":1000},{"type":"array","minItems":1,"items":{"type":"string"}}]},"choiceId":{"type":"string","minLength":1,"maxLength":1000},"tokens":{"type":"array","minItems":1,"items":{"type":"string"}}},"anyOf":[{"required":["answer"]},{"required":["choiceId"]},{"required":["tokens"]}],"additionalProperties":false},"ExerciseAnswerResult":{"type":"object","description":"Scored result. Incorrect submissions return coaching feedback and `canRetry: true` without disclosing `expectedAnswer`. The first correct completion reveals the answer and can award XP, including a correct retry. Later submissions return `alreadyCompleted: true` and `xpAwarded: 0`.","properties":{"exerciseId":{"type":"string"},"attemptId":{"type":"string","description":"Present when this submission created a scored attempt."},"type":{"type":"string","enum":["cloze","word_order","listening"]},"correct":{"type":"boolean"},"alreadyCompleted":{"type":"boolean"},"xpAwarded":{"type":"integer","minimum":0,"maximum":5},"balance":{"type":"integer","minimum":0},"mastery":{"$ref":"#/components/schemas/SkillMastery"},"expectedAnswer":{"type":"string","description":"Present only when this submission is correct."},"feedback":{"type":"string"},"canRetry":{"type":"boolean","description":"True after an incorrect, not-yet-completed attempt."}},"required":["exerciseId","type","correct","alreadyCompleted","xpAwarded","balance","mastery","feedback","canRetry"],"additionalProperties":false},"PracticeStats":{"type":"object","description":"Private vocabulary totals, recent sessions, and whether this account can currently earn another flash-card completion reward.","properties":{"totalWords":{"type":"integer","minimum":0},"dueWords":{"type":"integer","minimum":0},"familiarWords":{"type":"integer","minimum":0},"totalReviews":{"type":"integer","minimum":0},"rewardEligible":{"type":"boolean","description":"False after a rewarded completion in the same account-local day or the preceding rolling 24 hours."},"rewardDay":{"type":"string","format":"date","description":"Current calendar date in `rewardTimezone`."},"rewardTimezone":{"type":"string","description":"IANA timezone used for the flash-card reward day."},"sessions":{"type":"array","items":{"type":"object","additionalProperties":true}}},"required":["totalWords","dueWords","familiarWords","totalReviews","rewardEligible","rewardDay","rewardTimezone","sessions"],"additionalProperties":false},"PracticeSession":{"type":"object","description":"A session with card membership and due status snapshotted at start. Only cards due at that moment can contribute to this session's XP.","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["active"]},"totalCards":{"type":"integer","minimum":1,"maximum":20},"reviewedCards":{"type":"integer","minimum":0},"correctCards":{"type":"integer","minimum":0},"eligibleCards":{"type":"integer","minimum":0,"description":"Cards due when the session began."},"rewardEligible":{"type":"boolean","description":"Whether the session had a due card and the reward window was open at start. Eligibility is checked again on completion."},"rewardDay":{"type":"string","format":"date"},"rewardTimezone":{"type":"string"},"cards":{"type":"array","items":{"type":"object","additionalProperties":true,"description":"`xpEligible` marks cards that were due at session start."}},"createdAt":{"type":"string","format":"date-time"}},"required":["id","status","totalCards","reviewedCards","correctCards","eligibleCards","rewardEligible","rewardDay","rewardTimezone","cards","createdAt"],"additionalProperties":false},"PracticeReviewResult":{"type":"object","properties":{"sessionId":{"type":"string"},"wordId":{"type":"string"},"rating":{"type":"string","enum":["again","hard","good","easy"]},"mastery":{"type":"integer","minimum":0,"maximum":5},"nextReviewAt":{"type":"string","format":"date-time"},"cardXpEligible":{"type":"boolean","description":"Whether this card was due when the session began."},"reviewedCards":{"type":"integer","minimum":1},"correctCards":{"type":"integer","minimum":0},"totalCards":{"type":"integer","minimum":1},"completed":{"type":"boolean"},"eligibleCards":{"type":["integer","null"],"minimum":0,"description":"Populated when the session completes."},"correctEligibleCards":{"type":["integer","null"],"minimum":0,"description":"Due-at-start cards rated hard, good, or easy; populated on completion."},"rewardEligible":{"type":"boolean","description":"Final reward-window decision; meaningful when `completed` is true."},"xpAwarded":{"type":"integer","minimum":0,"maximum":15},"balance":{"type":"integer","minimum":0}},"required":["sessionId","wordId","rating","mastery","nextReviewAt","cardXpEligible","reviewedCards","correctCards","totalCards","completed","eligibleCards","correctEligibleCards","rewardEligible","xpAwarded","balance"],"additionalProperties":false}}},"paths":{"/api/v1/auth/register":{"post":{"tags":["Authentication"],"summary":"Create a learner account","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"displayName":{"type":"string"},"password":{"type":"string","format":"password"},"learningLanguage":{"type":"string","default":"fi"},"translationLanguage":{"type":"string","default":"en"}},"required":["email","displayName","password"]}}}},"responses":{"201":{"description":"Account and session created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/auth/login":{"post":{"tags":["Authentication"],"summary":"Create a session","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"identifier":{"type":"string","description":"Username or email address"},"password":{"type":"string","format":"password"},"deviceName":{"type":"string"}},"required":["identifier","password"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["Authentication"],"security":[{"bearerAuth":[]}],"summary":"End the current browser session","responses":{"204":{"description":"Signed out"}}}},"/api/v1/me":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"Get the current account","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"patch":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"Update profile, language pair, timezone, and daily XP goal","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"displayName":{"type":"string"},"uiLanguage":{"type":"string"},"profileTranslationExcluded":{"type":"boolean","default":true},"learningLanguage":{"type":"string"},"translationLanguage":{"type":"string"},"timezone":{"type":"string"},"dailyGoal":{"type":"integer","minimum":5,"maximum":100}},"required":[]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"delete":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"Delete the current account","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string"}},"required":["password"]}}}},"responses":{"204":{"description":"Account deleted"}}}},"/api/v1/me/password":{"put":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"Change password and revoke other sessions","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string"}},"required":["currentPassword","newPassword"]}}}},"responses":{"204":{"description":"Password changed"}}}},"/api/v1/me/daily-visit":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Claim the daily app-visit reward","description":"Awards 5 XP at most once per account-local calendar day, updates the learning streak, and records the grant in the private XP ledger. Repeated calls on the same local day are idempotent and award zero XP.","responses":{"200":{"description":"Daily visit reward and current balance","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/me/sessions":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"List active sessions","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/me/api-keys":{"get":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"List API keys","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"post":{"tags":["Account"],"security":[{"bearerAuth":[]}],"summary":"Create a scoped API key","description":"Available scopes: account:read, account:write, stories:read, stories:write, progress:read, progress:write, translations:read, translations:write, vocabulary:read, vocabulary:write, users:read, users:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"expiresAt":{"type":"string","format":"date-time"}},"required":["name"]}}}},"responses":{"201":{"description":"Key created; the full token is returned once","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/stories":{"get":{"tags":["Stories"],"security":[{"bearerAuth":[]}],"summary":"List localized stories enabled for the account","description":"Learners see only published stories with an available learning-language presentation and only enabled segments are counted. Editor/owner accounts may preview source metadata and other lifecycle states.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["draft","published","archived","all"]},"description":"Honored only for editor/owner accounts; learners always receive published content."}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/stories/{storyId}":{"get":{"tags":["Stories"],"security":[{"bearerAuth":[]}],"summary":"Get a localized story, segment capabilities, glossary, and private progress","description":"Learners receive only the published learning-language presentation. Disabled or unavailable translated segments are omitted. Future locked segments expose only ID, position, a locked label, disabled capabilities, and locked progress; text, questions, choices, glossary, rewards, and other content remain redacted until unlocked. Canonical English source, unpublished translations, source choice text, correctness flags, and glossary translations are included only for editor/owner previews.","parameters":[{"name":"storyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/stories/{storyId}/start":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Start or resume at the next eligible story segment","description":"For learners, unlocks the first enabled, available-in-the-learning-language, incomplete segment and skips disabled or unavailable segments. Only that available segment is returned with learner content; later segments remain redacted until progression unlocks them. If every eligible segment is already complete, the completed story is returned unchanged. Editor/owner accounts retain their role-gated source and draft preview.","parameters":[{"name":"storyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/curriculum/domains":{"get":{"tags":["Curriculum"],"security":[{"bearerAuth":[]}],"summary":"List reusable language-agnostic content domains","description":"Returns the canonical domain catalog used to group CEFR blocks and language-specific content items.","responses":{"200":{"description":"Curriculum domains","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CurriculumDomain"}}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/curriculum/blocks":{"get":{"tags":["Curriculum"],"security":[{"bearerAuth":[]}],"summary":"List CEFR focus blocks","description":"Filters language-agnostic blocks by domain, CEFR level, exam profile, or available item language. Each block includes its CEFR-level metadata, functions, lexical sets, and item counts.","parameters":[{"name":"domainId","in":"query","schema":{"type":"string"}},{"name":"cefrLevel","in":"query","schema":{"type":"string","enum":["A1","A2","B1","B2","C1","C2"]}},{"name":"examId","in":"query","schema":{"type":"string"}},{"name":"language","in":"query","schema":{"type":"string","example":"fi"}}],"responses":{"200":{"description":"Curriculum blocks","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CurriculumBlock"}}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/curriculum/blocks/{blockId}":{"get":{"tags":["Curriculum"],"security":[{"bearerAuth":[]}],"summary":"Get a CEFR focus block and its content items","parameters":[{"name":"blockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"language","in":"query","schema":{"type":"string","example":"fi"}}],"responses":{"200":{"description":"Curriculum block detail","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CurriculumBlock"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/curriculum/exam-profiles":{"get":{"tags":["Curriculum"],"security":[{"bearerAuth":[]}],"summary":"List exam profiles mapped onto CEFR and blocks","description":"Exam-specific selection remains data-driven: profiles expose CEFR range, modality, preferred domains/functions, confidence, and weighted block preferences.","responses":{"200":{"description":"Exam profiles","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExamProfile"}}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/progress":{"get":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Get story completion and audited XP ledger","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/learning/plan":{"get":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Get the authenticated learner's private adaptive plan","description":"Returns learner-local daily XP progress, a rolling 24-hour hint quota, five skill signals, due words, and story recommendations. Recommendations and exercises are restricted to published stories and per-segment capabilities enabled for this account. The plan `date` uses the account timezone; hint `resetsOn` is an absolute timestamp independent of timezone changes.","responses":{"200":{"description":"Adaptive plan","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LearningPlan"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/exercises":{"get":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"List private-answer adaptive exercises","description":"Deterministically generates practice from published story content enabled for the authenticated learner. Incomplete work and weaker skills are prioritized, then exercise types are interleaved. Responses never include an expected answer or a choice correctness flag.","parameters":[{"name":"types","in":"query","description":"Comma-separated subset. Omit for all supported types.","schema":{"type":"string","example":"cloze,word_order,listening"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":12}}],"responses":{"200":{"description":"Adaptive exercises without answer keys","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdaptiveExercise"}}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/exercises/{id}/answer":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Score an adaptive exercise","description":"An incorrect submission updates mastery and returns actionable feedback with `canRetry: true`, but does not expose the answer key. The first correct completion reveals `expectedAnswer` and awards 5 XP, including when it follows incorrect retries. XP is idempotent per learner and exercise. Once completed, later submissions do not create another scored attempt or change mastery and return `alreadyCompleted: true` with zero XP.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExerciseAnswerRequest"}}}},"responses":{"200":{"description":"Scored answer and post-submission feedback","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ExerciseAnswerResult"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/segments/{segmentId}/question/reveal":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Reveal a comprehension-question attempt","description":"Charges 5 XP to reveal the question and choices for an available story segment. Reopening an already visible or completed question is free. Concurrent calls are idempotent for the same attempt.","parameters":[{"name":"segmentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revealed localized question, choices, cost, and current balance","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/segments/{segmentId}/answer":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Answer the revealed segment choice","description":"Requires an unlocked segment and a paid, currently revealed question attempt. An incorrect choice increments reading/comprehension attempts, hides the question, and requires another 5 XP reveal. The first correct answer awards the configured segment XP, unlocks the next eligible incomplete segment, and may complete the story. Repeats after completion award zero XP. Locked and hidden state is validated before choice membership.","parameters":[{"name":"segmentId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"choiceId":{"type":"string"}},"required":["choiceId"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/segments/{segmentId}/complete":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"summary":"Complete an available reading-only segment","description":"Use only when the segment has no choices or choices are disabled for this learner. Records a successful reading attempt, awards the configured segment XP once, unlocks the next eligible incomplete segment, and may complete the story. If effective choices are enabled, answer the question instead; locked segments return a conflict without exposing content.","parameters":[{"name":"segmentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reading completion and next-segment state","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/translate":{"post":{"tags":["Learning"],"security":[{"bearerAuth":[]}],"deprecated":true,"summary":"Legacy reviewed story translation hint","description":"Translates only content with a reviewed glossary, story, or choice translation on an unlocked or completed segment; this is not a free-form machine-translation endpoint. Each authenticated learner gets five free new hints in a rolling 24-hour window. Case, punctuation, and whitespace variants share an account-private canonical cache entry, remain free, and do not consume quota again. After the allowance is used, a new hint costs the segment's configured XP amount. Quota `resetsOn` is the absolute timestamp when the oldest active free-hint slot expires and is unaffected by account-timezone changes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"segmentId":{"type":"string"},"text":{"type":"string"},"fromLanguage":{"type":"string","default":"fi"},"toLanguage":{"type":"string","default":"en"}},"required":["segmentId","text"]}}}},"responses":{"200":{"description":"Reviewed translation with current balance and rolling quota","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TranslationResult"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/tooltips":{"get":{"tags":["Translations"],"security":[{"bearerAuth":[]}],"summary":"List private purchased tooltips","description":"Returns global interface-element purchases and, when storyId is supplied, purchases tied to that accessible story. User names and account identifiers are never tooltip targets.","parameters":[{"name":"storyId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Purchased tooltips","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TooltipPurchase"}}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/tooltips/preview":{"post":{"tags":["Translations"],"security":[{"bearerAuth":[]}],"summary":"Quote an XP tooltip purchase","description":"Expands through overlapping or directly adjacent paid ranges and charges only unpaid words. Elements and single words cost 1 XP; multi-word ranges cost ceil(unpaid × 0.9); full paragraphs cost ceil(unpaid × 0.7). Identical interface elements are account-global while story words remain occurrence-specific.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooltipTarget"}}}},"responses":{"200":{"description":"Validated selection and current price","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TooltipPurchase"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/tooltips/purchase":{"post":{"tags":["Translations"],"security":[{"bearerAuth":[]}],"summary":"Purchase or merge an XP tooltip","description":"Atomically revalidates the quote, deducts XP, records the ledger entry, and persists the private tooltip. Reopening an exact purchase is free; overlapping or directly adjacent ranges merge and charge only their unpaid indices.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TooltipTarget"}}}},"responses":{"200":{"description":"Purchased tooltip and updated balance","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TooltipPurchase"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/tooltips/{id}":{"delete":{"tags":["Translations"],"security":[{"bearerAuth":[]}],"summary":"Undo a purchased XP tooltip","description":"Deletes the account-private tooltip and refunds 50% of its stored XP cost, rounded up to whole points. The refund is recorded in the private XP ledger.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Tooltip removed and current balance returned","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TooltipUndoResult"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/vocabulary":{"get":{"tags":["Vocabulary"],"security":[{"bearerAuth":[]}],"summary":"List saved words","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"post":{"tags":["Vocabulary"],"security":[{"bearerAuth":[]}],"summary":"Save a word","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"term":{"type":"string"},"translation":{"type":"string"},"storyId":{"type":"string"},"segmentId":{"type":"string"}},"required":["term","translation"]}}}},"responses":{"201":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/vocabulary/practice":{"get":{"tags":["Vocabulary"],"security":[{"bearerAuth":[]}],"summary":"Get spaced-repetition stats and reward eligibility","description":"Returns only the authenticated learner's word totals and recent sessions. A rewarded flash-card completion is available at most once per account-local calendar day and no more than once in any rolling 24-hour window.","responses":{"200":{"description":"Practice statistics and cooldown state","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PracticeStats"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/vocabulary/practice/start":{"post":{"tags":["Vocabulary"],"security":[{"bearerAuth":[]}],"summary":"Start a snapshotted flash-card session","description":"Seeds an enabled-story starter pack when needed and abandons any prior active session. Card membership and due status are frozen for integrity. A session is reward-eligible only when the reward window is open and at least one selected card is due at start; non-due cards may still be practiced and rescheduled but cannot contribute XP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","minimum":4,"maximum":20,"default":8}},"required":[]}}}},"responses":{"201":{"description":"Practice session started","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PracticeSession"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/vocabulary/practice/{sessionId}/review":{"post":{"tags":["Vocabulary"],"security":[{"bearerAuth":[]}],"summary":"Rate one card and advance its schedule","description":"The word must belong to this session and can be reviewed once. `again` schedules about 10 minutes, `hard` at least 12 hours, `good` at least one day, and `easy` at least three days; later intervals scale from the prior interval. On completion, only due-at-start cards rated hard/good/easy count: 2 XP each, plus 3 XP when every eligible card passes, capped at 15 XP. The account-local-day and rolling-24-hour reward window is rechecked at completion.","parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"wordId":{"type":"string"},"rating":{"enum":["again","hard","good","easy"]}},"required":["wordId","rating"]}}}},"responses":{"200":{"description":"Updated schedule and session reward state","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PracticeReviewResult"}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/vocabulary/games/match-up/complete":{"post":{"tags":["Vocabulary"],"security":[{"bearerAuth":[]}],"summary":"Convert a completed match-up score to XP","description":"Awards the positive net point total from a fully mastered 3–8 word Match up game. Points must equal correct attempts minus misses, the words must belong to the learner, and the client-generated game identifier makes retries idempotent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gameId":{"type":"string","minLength":8,"maxLength":80},"wordIds":{"type":"array","minItems":3,"maxItems":8,"uniqueItems":true,"items":{"type":"string"}},"points":{"type":"integer","minimum":-500,"maximum":500},"moves":{"type":"integer","minimum":0,"maximum":500},"mistakes":{"type":"integer","minimum":0,"maximum":500}},"required":["gameId","wordIds","points","moves","mistakes"]}}}},"responses":{"200":{"description":"Ledger-backed XP award and current balance","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/users":{"get":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"List accounts","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"post":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"Create an account","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"displayName":{"type":"string"},"role":{"enum":["learner","editor","owner"]},"learningLanguage":{"type":"string"},"translationLanguage":{"type":"string"}},"required":["email","displayName"]}}}},"responses":{"201":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/users/{userId}":{"get":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"Get account, access settings, and progress","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"patch":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"Update account, role, status, language pair, or XP","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}},"delete":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"Delete another account and its learner data","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Account deleted"}}}},"/api/v1/admin/users/{userId}/reset-password":{"post":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"Revoke sessions and issue a must-change temporary password","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","description":"Optional; a random temporary password is generated when omitted."}},"required":[]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/users/{userId}/access":{"put":{"tags":["User management"],"security":[{"bearerAuth":[]}],"summary":"Enable stories and per-segment items for a user","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"storyId":{"type":"string"},"segmentId":{"type":"string"},"enabled":{"type":"boolean"},"settings":{"$ref":"#/components/schemas/SegmentCapabilities"}},"required":[]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/stories":{"post":{"tags":["Story studio"],"security":[{"bearerAuth":[]}],"summary":"Create a canonical English story, optionally with nested translations, segments, choices, and glossary","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"summary":{"type":"string"},"sourceLanguage":{"const":"en"},"cefrLevel":{"type":"string"},"translations":{"type":"array","items":{"type":"object"}},"segments":{"type":"array","items":{"type":"object"}}},"required":["title"]}}}},"responses":{"201":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/stories/{storyId}":{"patch":{"tags":["Story studio"],"security":[{"bearerAuth":[]}],"summary":"Update story metadata or lifecycle","parameters":[{"name":"storyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"summary":{"type":"string"},"theme":{"type":"string"},"cefrLevel":{"type":"string"},"status":{"enum":["draft","published","archived"]}},"required":[]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/stories/{storyId}/segments":{"post":{"tags":["Story studio"],"security":[{"bearerAuth":[]}],"summary":"Add an English segment with choices and localized versions","parameters":[{"name":"storyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"sourceText":{"type":"string"},"question":{"type":"string"},"choices":{"type":"array","items":{"type":"object"}},"translations":{"type":"array","items":{"type":"object"}},"glossary":{"type":"array","items":{"type":"object"}}},"required":["title","sourceText","question"]}}}},"responses":{"201":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/stories/{storyId}/translations/{language}":{"put":{"tags":["Story studio"],"security":[{"bearerAuth":[]}],"summary":"Upsert a story translation","parameters":[{"name":"storyId","in":"path","required":true,"schema":{"type":"string"}},{"name":"language","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"summary":{"type":"string"},"status":{"enum":["draft","review","published"]}},"required":["title"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}},"/api/v1/admin/segments/{segmentId}/translations/{language}":{"put":{"tags":["Story studio"],"security":[{"bearerAuth":[]}],"summary":"Upsert a complete segment translation and localized choices","parameters":[{"name":"segmentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"language","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"text":{"type":"string"},"question":{"type":"string"},"correctFeedback":{"type":"string"},"wrongFeedback":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"status":{"type":"string"}},"required":["title","text","question"]}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{}},"required":["data"],"additionalProperties":false}}}}}}}}}