# intouch api base https://api.in-touch.cc auth X-API-Key: itk_<64 hex> one key per user, no expiry, no scopes docs https://docs.in-touch.cc format every response is JSON and carries "success" # vocabulary status: stable An entirely AI-driven language-learning capability. The app has NO settings screen for it: language, level and daily volume are agreed with the user in conversation and written through the API. The app downloads the whole set INCLUDING its solutions and grades on-device, so the user can practise offline. Results are queued locally and uploaded on the next connection. A push REPLACES the active set completely; history survives. # the daily loop 1. GET /api/addons/vocab/profile status only: is today's set already there? 2. GET /api/addons/vocab/history what went wrong, which types are weak, how many tries did it take 3. POST /api/addons/vocab/push today's set, carrying language + level: recycled mistakes + new material, 5 types THERE IS NO SERVER-SIDE PROFILE. Language, level and questions-per-day live in the agent's own prompt file on the user's machine. Raising the level means editing that file; the next push carries the new value. The app reads language and level from the active set. IDEMPOTENCY GUARD - this is the important one: if active_set.set_date is already today (UTC), STOP before writing anything. Never push twice on the same day. That guard is what makes a laptop schedule safe: it may fire at breakfast, at lunch, or twice. # endpoints GET /api/addons/vocab/profile X-API-Key status: active set, streak POST /api/addons/vocab/push X-API-Key replace the active set GET /api/addons/vocab/history X-API-Key read every answer # GET /api/addons/vocab/profile (historical path name, status only) curl "https://api.in-touch.cc/api/addons/vocab/profile" -H "X-API-Key: itk_YOUR_KEY" 200 { "success": true, "supported_types": ["translate","multiple_choice","fill_gap", "match_pairs","sentence_order"], "active_set": { "id": "vs_42_1753400000000", "language": "es", "level": "A2", "set_date": "2026-07-25", "question_count": 8 }, "streak": 6 } "active_set": null -> nothing has ever been pushed for this user. Compare active_set.set_date with today (UTC) before every push. There is no profile field: what to teach comes from your prompt file, not from the server. # POST /api/addons/vocab/push questions[] array REQUIRED 1-50 questions, see types below language string REQUIRED e.g. "es" - from your prompt file; the app labels the feed card with it level string A1-C1, defaults to A1 set_date string YYYY-MM-DD, defaults to today set_id string own identifier; generated if omitted # question types Every question object needs "type"; the remaining fields are its payload. * = required. Invalid questions are reported per index in errors[]; the rest is still accepted. translate prompt*, accepted*(string[]), direction, hint, explanation multiple_choice prompt*, options*(>=2), correct_index*, explanation fill_gap sentence*(must contain ___), accepted*(string[]), word_bank(string[]), explanation match_pairs pairs*(>=2 of {left, right}), explanation sentence_order solution*(string[], >=2), tokens(same words, shuffled), explanation Free-text answers are matched LENIENTLY: translate and fill_gap compare against "accepted" ignoring case, accents, punctuation and repeated spaces - but NOT synonyms. List every reasonable variant in "accepted". A word_bank on fill_gap turns it into a tap-to-answer grammar drill, which is the natural way to test conjugation. # push example curl -X POST https://api.in-touch.cc/api/addons/vocab/push \ -H "X-API-Key: itk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "es", "level": "A2", "questions": [ { "type": "translate", "prompt": "der Schlüssel", "direction": "Deutsch → Spanisch", "accepted": ["la llave","llave"], "explanation": "llave ist feminin." }, { "type": "multiple_choice", "prompt": "¿Cómo ___ (tú)?", "options": ["estás","está","estoy"], "correct_index": 0 }, { "type": "fill_gap", "sentence": "Ayer ___ al mercado.", "accepted": ["fui"], "word_bank": ["fui","voy","iré"], "explanation": "Indefinido, 1. Person Singular von ir." }, { "type": "match_pairs", "pairs": [ { "left": "el pan", "right": "das Brot" }, { "left": "la leche", "right": "die Milch" } ] }, { "type": "sentence_order", "solution": ["Mañana","voy","a","la","playa"], "tokens": ["playa","voy","Mañana","la","a"] } ] }' 200 { "success": true, "set_id": "vs_42_1753400000000", "language": "es", "level": "A2", "set_date": "2026-07-25", "accepted": 5, "rejected": 0, "errors": [] } # GET /api/addons/vocab/history The durable memory of this capability: every answer ever given, with the question it belonged to, what the user typed, right or wrong, and how many attempts it took. Survives set replacement, reinstall and a new machine. query: only wrong | correct | all (default all) limit 1-500, default 100 since cursor from a previous response: only what was answered after it, oldest first. Opaque - hand back exactly the string you got. A question answered again after "practise again" reappears: its timestamp moved, and so did its attempt count. curl "https://api.in-touch.cc/api/addons/vocab/history?only=wrong&limit=50" \ -H "X-API-Key: itk_YOUR_KEY" 200 { "success": true, "streak": 6, "summary": { "total": 96, "correct": 71, "accuracy": 74, "first_try": 58, "first_try_rate": 60, "avg_attempts": 1.34, "by_type": { "translate": { "total": 40, "correct": 25, "accuracy": 63, "first_try": 19, "avg_attempts": 1.52 } } }, "hardest": [ { "question_id": 512, "type": "translate", "attempts": 4, "correct": true, "question": { "prompt": "der Schlüssel", "accepted": ["la llave"] } } ], "items": [ { "question_id": 512, "set_date": "2026-07-24", "type": "translate", "question": { "prompt": "der Schlüssel", "accepted": ["la llave"] }, "user_answer": "la clave", "correct": false, "attempts": 4, "answered_at": "2026-07-24T18:22:10Z" } ] } accuracy and first_try_rate are PERCENT (0-100). avg_attempts is tries per question. hardest[] holds up to 10 questions with the most attempts, newest first - the most direct "teach this again" list. # attempts - the signal behind the accuracy The app has a "practise again" button that reopens every question of the set. attempts counts how many tries a question needed UNTIL IT WAS FIRST ANSWERED CORRECTLY. items[].attempts 1 = right first time; higher = that many goes summary.first_try(_rate) answers that landed on the first attempt summary.avg_attempts average tries per question, overall / per type hardest[] worst 10 by attempts, newest first A question answered correctly on the third attempt still counts as correct: true - only attempts reveals the word is not settled. Weight hardest[] and first_try_rate more heavily than raw accuracy when choosing what to repeat. The counter stops once a question was answered correctly and never decreases, so a late offline batch cannot overwrite a higher count. # what lives where server (durable, readable with the key, survives everything): every answer: question snapshot, the user's input, correct/wrong, attempts, the date - plus the streak and the active set local prompt file ~/intouch/vocab-prompt.md (the user owns and can edit it): language, level, questions per day, the "learned:" conclusions you write after reading the history, and "last read:" (your history cursor) # reading only what changed Store "cursor" from the response, pass it back as ?since= next time, and keep calling with the newest cursor while has_more is true. Nothing is deleted; you are simply not re-reading years of answers every morning. Without since you get the current picture, newest first - use that for the standing summary. Rule: raw events belong on the server, conclusions belong in the file. Never copy the whole history into the file; re-read it and rewrite the conclusions. # level decision (suggested policy, only once summary.total >= 30) first_try_rate >= 85 and avg_attempts <= 1.2 -> one level up (A1 A2 B1 B2 C1) first_try_rate <= 40 -> one level down (never below A1) otherwise -> keep it Apply it by editing the level in YOUR OWN prompt file and sending the new value with the next push. Nothing to store server-side. # building a good set - Around 40% revisits what went wrong or needed several attempts: take it from hardest[] and only=wrong, but REPHRASE it - the user should not recognise the wording. - The rest is new material for the current level, reusing known vocabulary. - Use all five types, over-weighting the types with the lowest accuracy. - Every question carries its solution and a short explanation in the user's native language. - Verify the response: if anything was rejected, fix those questions and push the corrected FULL set again (a push replaces, it never appends). # offline behaviour and replacement push while the user is offline they keep the old set; the new one arrives when they reconnect push a new set_id pending results upload first, then the old set and its local progress are discarded push twice on the same day the second set replaces the first; a day already completed stays counted for the streak user answers offline graded on-device, queued, uploaded on the next connection - history catches up afterwards user taps "practise again" every question reopens; attempts keeps counting, the streak is untouched DO NOT push a fresh set every few minutes: each push wipes the set the user is currently working through. One set per day, or when history shows the previous one is finished, is the intended rhythm. # streaks A day counts once EVERY question of that day's set has been answered, right or wrong. The streak is the number of consecutive such days. A day still in progress does not break it, so a user who has not practised yet today keeps yesterday's streak until midnight UTC. # automation The human page has ready-made setups for Claude Code and Codex CLI plus schedulers for macOS (launchd), Linux/VPS (systemd timer, cron), Windows (Task Scheduler) and GitHub Actions: https://docs.in-touch.cc/vocabulary#automate The pattern in one line: a guard script asks GET /vocab/profile whether active_set.set_date is today and only wakes the model if it is not. # self-setup - read this when a user asks you to "set up my vocabulary training" They pasted a short prompt and their key; the whole job is below. Work through it in order and do not invent extra steps, files, services or repositories. 0. THE KEY IS A CREDENTIAL. Never print it, never echo it into a log, never put it in the prompt file. It belongs in the guard script or an environment variable. 1. ASK - one message, four questions, with defaults so a one-word answer is enough: target language, level (A1-C1, default A2), questions per day (3-50, default 8), language for explanations (default: the user's own language). If the user already answered all of that in their prompt, skip the questions. 2. WRITE THE PROMPT FILE - ~/intouch/vocab-prompt.md This file IS the profile; the server stores none. Put the four answers in a header block ("language", "my language", "level", "questions per day") plus an empty "learned:" line, and below them the daily loop, the level decision policy and "building a good set" from this page, so a future run needs nothing but this file. Add a "last read:" line for the history cursor. Every later run reads only what is new (?since=), refreshes the "learned:" lines and overwrites the cursor - that is how the file adapts without re-reading everything. Show it to the user before writing it. 3. PUSH A FIRST SET Skip if active_set.set_date is already today (UTC). Send "language" and "level" from the header block with the push - language is required. Build the set per "building a good set" and "question types" above. There is no history yet, so this first set is plain new material for the level. Read the response: fix and re-push the FULL set if anything was rejected. 4. INSTALL THE DAILY AUTOMATION Show the user the file paths and the schedule BEFORE creating anything, then: a) the prompt file from step 2 is already in place. b) ~/intouch/vocab.sh (chmod +x) - the guard: - read GET /api/addons/vocab/profile - API unreachable -> exit 0 (a machine that just woke has no network yet) - active_set.set_date == today (UTC, date -u +%F) -> exit 0 - otherwise run your own CLI headless with the prompt file c) the scheduler that fits the user's OS - always one that CATCHES UP after a missed run, because a laptop is asleep at 07:00: macOS LaunchAgent ~/Library/LaunchAgents/com.intouch.vocab.plist, StartCalendarInterval 07:00 + RunAtLoad, launchctl load -w Linux systemd user timer, OnCalendar=*-*-* 07:00:00, Persistent=true, systemctl --user enable --now; on a VPS also loginctl enable-linger Windows Register-ScheduledTask with -StartWhenAvailable, or WSL + the Linux path if the CLI lives there no local shell -> a GitHub Actions workflow on a daily cron instead The exact, tested snippets for all four are on https://docs.in-touch.cc/vocabulary#automate. 5. VERIFY - run the guard script once by hand. It must report that today's set already exists, because step 3 created it. That proves the guard works, which is the part that keeps a repeated run harmless. 6. HAND OVER - name the two files, the scheduler entry and the next run time, and tell the user that changes ("harder", "switch to Italian", "more questions") are four lines at the top of ~/intouch/vocab-prompt.md - by talking to you again, or in any text editor. Neither the app nor the server has a setting. If you cannot run shell commands (browser chat, no terminal): do step 1, push a first set (step 3), then tell the user plainly that the prompt file and the daily schedule need Claude Code, Codex CLI or a cloud runner, and point them at https://docs.in-touch.cc/vocabulary#automate. # self-setup: working snippets Adapt paths and the CLI line; the shape is what matters. --- ~/intouch/vocab.sh ------------------------------------------------------- #!/usr/bin/env bash set -euo pipefail export INTOUCH_API_KEY="itk_THE_USERS_KEY" BASE="https://api.in-touch.cc" TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT today="$(date -u +%F)" # No network yet (the machine just woke up) is not an error. A rejected key is: # it has to fail loudly, or the schedule quietly does nothing for weeks. code="$(curl -sS -o "$TMP" -w '%{http_code}' \ -H "X-API-Key: $INTOUCH_API_KEY" \ "$BASE/api/addons/vocab/profile")" || { echo "inTouch: API not reachable, leaving it for the next run"; exit 0 } case "$code" in 200) ;; 401|403) echo "inTouch: key rejected ($code) - fix it in this script"; exit 1 ;; *) echo "inTouch: API returned $code, trying again next run"; exit 0 ;; esac if [ "$(jq -r '.active_set.set_date // ""' "$TMP")" = "$today" ]; then echo "inTouch: set for $today already exists" exit 0 fi # your own CLI, headless, allowed to reach the network. For example: # claude -p "$(cat "$HOME/intouch/vocab-prompt.md")" --allowedTools "Bash(curl:*)" # codex exec --full-auto "$(cat "$HOME/intouch/vocab-prompt.md")" ------------------------------------------------------------------------------ --- macOS: ~/Library/LaunchAgents/com.intouch.vocab.plist -------------------- Label com.intouch.vocab ProgramArguments /bin/bash /Users/YOU/intouch/vocab.sh StartCalendarInterval Hour7Minute0 RunAtLoad StandardOutPath /tmp/intouch-vocab.log StandardErrorPath /tmp/intouch-vocab.log launchctl load -w ~/Library/LaunchAgents/com.intouch.vocab.plist StartCalendarInterval catches up after sleep; RunAtLoad adds a run at login. ------------------------------------------------------------------------------ --- Linux / VPS: systemd user timer ------------------------------------------ # ~/.config/systemd/user/intouch-vocab.service [Unit] Description=inTouch vocabulary set [Service] Type=oneshot ExecStart=%h/intouch/vocab.sh # ~/.config/systemd/user/intouch-vocab.timer [Unit] Description=Daily inTouch vocabulary set [Timer] OnCalendar=*-*-* 07:00:00 Persistent=true [Install] WantedBy=timers.target systemctl --user daemon-reload systemctl --user enable --now intouch-vocab.timer # VPS without a login session: loginctl enable-linger $USER Persistent=true is the catch-up after a missed run. ------------------------------------------------------------------------------ --- Windows: Task Scheduler -------------------------------------------------- $action = New-ScheduledTaskAction -Execute "powershell.exe" ` -Argument "-File C:\intouch\vocab.ps1" $trigger = New-ScheduledTaskTrigger -Daily -At 7am $set = New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun Register-ScheduledTask -TaskName "inTouch Vocab" -Action $action ` -Trigger $trigger -Settings $set -StartWhenAvailable is the catch-up. If the CLI lives in WSL, keep the bash script and let the task call: wsl -e ~/intouch/vocab.sh ------------------------------------------------------------------------------ --- No local machine: GitHub Actions ----------------------------------------- # .github/workflows/vocab.yml (private repo, secrets for both keys) name: inTouch Vocab on: schedule: - cron: "0 5 * * *" workflow_dispatch: jobs: push-set: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm install -g @anthropic-ai/claude-code - run: claude -p "$(cat vocab-prompt.md)" --allowedTools "Bash(curl:*)" env: ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} INTOUCH_API_KEY: \${{ secrets.INTOUCH_API_KEY }} Scheduled runs can start a few minutes late, and GitHub disables the schedule after 60 days without repository activity. ------------------------------------------------------------------------------ # errors 401 missing X-API-Key header 403 invalid API key 400 no valid question, unknown type, or missing language 500 server error # pages / overview, capability list /quickstart get a key, first request /authentication key format, rotation, failure modes /conventions envelope, status codes, limits, dates /recipes recipes capability /vocabulary vocabulary capability /roadmap capability anatomy, what is planned every page is also plain text: append .txt (e.g. https://docs.in-touch.cc/vocabulary.txt) all pages at once: /llms.txt (index) and /llms-full.txt (everything) # for agents - Use only the endpoints listed here; do not invent paths, fields or values. - Check "success" in the body, not just the HTTP status. - Error strings are German and developer-facing; branch on the status code. - Batches are accepted partially: read "rejected" and "errors[].index". - Dates are UTC. Calendar days are YYYY-MM-DD, timestamps are ISO 8601. - The API key is a credential: never log it, never pass it to a third party. - Ask the user before writing content into their app for the first time.