# 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" # recipes status: stable Send meal suggestions into a user's feed, read back what they liked. The user swipes through the suggestions per meal category; the ones they like become a day plan. Loop: suggest, observe, suggest better. # the daily loop 1. GET /api/addons/recipe/history?date= are today's dishes already there? 2. GET /api/addons/recipe/history?state=liked what worked, what was rejected 3. POST /api/addons/recipe/suggest today's dishes per meal category IDEMPOTENCY GUARD - this one matters more than in most APIs: suggest APPENDS, it does not replace. If history for today already returns summary.total > 0, STOP. A second run doubles every meal in the user's feed. THERE IS NO SERVER-SIDE PROFILE. Diet, allergies, household size and the meals the user wants live in the agent's own prompt file on the user's machine. The server stores only the suggestions themselves and the like/dislike decisions. # endpoints POST /api/addons/recipe/suggest X-API-Key push suggestions GET /api/addons/recipe/history X-API-Key read decisions # POST /api/addons/recipe/suggest body: recipes[] array REQUIRED one or more recipes, max 50 per request plan_date string YYYY-MM-DD, defaults to today; applies to recipes without their own plan_date A bare array or a single recipe object is accepted too; the "recipes" wrapper is the clearest form. # recipe object meal_category string REQUIRED one of the categories below title string REQUIRED dish name image string image URL description string short description duration string "25 min" difficulty string "Easy" servings number portions ingredients string[] ingredient list steps string[] preparation steps tags string[] ["vegan","gluten-free"] nutrition object { calories, protein, carbs, fat } Unknown fields are stored and handed back verbatim by history - use them for your own metadata. # meal_category (exact values) breakfast brunch lunch dinner snack drink dessert sidedish Anything else rejects that ITEM; the rest of the batch still goes through. # example curl -X POST https://api.in-touch.cc/api/addons/recipe/suggest \ -H "X-API-Key: itk_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "plan_date": "2026-07-25", "recipes": [ { "meal_category": "breakfast", "title": "Overnight Oats with Berries", "image": "https://example.com/oats.jpg", "description": "Creamy oats soaked overnight.", "duration": "5 min + overnight", "difficulty": "Easy", "servings": 1, "ingredients": ["50g oats","150ml milk","berries"], "steps": ["Mix","Chill overnight","Top with berries"], "tags": ["vegetarian","meal-prep"], "nutrition": { "calories": 320, "protein": "12g", "carbs": "48g", "fat": "8g" } } ] }' 200 { "success": true, "inserted": 1, "rejected": 0, "plan_date": "2026-07-25" } # GET /api/addons/recipe/history The durable memory of this capability: every dish ever sent, with what the user did about it. Survives new pushes, reinstalls and a new machine. query: state liked | disliked | open | all (default all) date optional YYYY-MM-DD filter limit 1-500, default 100 since cursor from a previous response: only what was ADDED or DECIDED after it, oldest first. Opaque - hand back exactly the string you got. curl "https://api.in-touch.cc/api/addons/recipe/history?state=liked&limit=50" -H "X-API-Key: itk_YOUR_KEY" 200 { "success": true, "state": "liked", "summary": { "total": 42, "liked": 9, "disliked": 12, "open": 21, "returned": 9 }, "cursor": "2026-07-26 18:04:11", "has_more": false, "dishes": [ { "id": 123, "meal_category": "dinner", "plan_date": "2026-07-25", "state": "liked", "recipe": { "title": "Oven Salmon", "...": "..." } } ] } # 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. You get new suggestions AND decisions on older dishes - a heart tapped on last month's dinner appears, because the cursor follows when something was DECIDED, not when it was created. Nothing is ever deleted; you are simply not re-reading it. Without since you get the current picture, newest first. summary always covers the WHOLE date scope and every state, never just the page you asked for - otherwise ?state=liked would report disliked: 0 and you would read the taste as one-sided. "returned" is how many dishes are in this response. state meanings: liked the user tapped the heart - "I'll cook this today" disliked explicitly rejected open delivered, not decided yet # what lives where server (durable, readable with the key, survives everything): every suggestion, its meal category, its date, and liked/disliked/open local prompt file ~/intouch/recipe-prompt.md (the user owns and can edit it): the DISTILLED preferences - diet, allergies, household, the "learned:" lines you write after reading the history, and "last read:" (your history cursor) 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. # offline behaviour The app keeps the last plan it successfully loaded and shows it without a connection. It falls back to that copy ONLY when the server is unreachable, so a day you deliberately left empty stays empty instead of resurrecting yesterday. # self-setup - read this when a user asks you to "set up my meal suggestions" 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, with defaults so short answers are enough: how many people you cook for, diet (omnivore / vegetarian / vegan / other), allergies, hard no-gos, which meals per day (default breakfast, lunch, dinner), how many options per meal (default 2), and how much effort on a weekday. If the user already answered in their prompt, skip the questions. 2. WRITE THE PROMPT FILE - ~/intouch/recipe-prompt.md This file IS the profile; the server stores none. Put the answers in a header block ("cooking for", "diet", "allergies", "hard no", "meals per day", "options per meal", "effort", plus empty "learned:" and "last read:" lines) and below them the daily loop, the recipe object and the meal categories from this page, so a future run needs nothing but this file. Every later run reads only what is new (?since=), refreshes "learned:" and overwrites the cursor. Show it to the user before writing it. 3. PUSH A FIRST DAY Skip if history for today already has summary.total > 0. One entry per meal category, "options per meal" each, respecting diet and allergies absolutely. Read the response: fix rejected items and push ONLY those again - suggest appends, so never resend the whole batch. 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/recipes.sh (chmod +x) - the guard: - read GET /api/addons/recipe/history?date= - API unreachable -> exit 0 (a machine that just woke has no network yet) - 401/403 -> exit 1 loudly, the key is wrong - summary.total > 0 -> exit 0, today is done - otherwise run your own CLI headless with the prompt file c) the scheduler that fits the user's OS, from "working snippets" below. If the user also runs the vocabulary capability, these are two independent jobs: keep the distinct names below so neither replaces the other. 5. VERIFY - run the guard script once by hand. It must report that today's suggestions already exist, because step 3 created them. 6. HAND OVER - name the two files, the scheduler entry and the next run time, and tell the user that changes ("more vegetarian", "no lunch", "I moved in with someone") are a few lines at the top of ~/intouch/recipe-prompt.md - by talking to you again, or in any text editor. If you cannot run shell commands (browser chat, no terminal): do step 1, push a first day (step 3), then tell the user plainly that the prompt file and the daily schedule need Claude Code, Codex CLI or a cloud runner. # self-setup: working snippets Adapt paths and the CLI line; the shape is what matters. --- ~/intouch/recipes.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/recipe/history?date=$today")" || { 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 '.summary.total // 0' "$TMP")" -gt 0 ]; then echo "inTouch: suggestions for $today already exist" exit 0 fi # your own CLI, headless, allowed to reach the network. For example: # claude -p "$(cat "$HOME/intouch/recipe-prompt.md")" --allowedTools "Bash(curl:*)" # codex exec --full-auto "$(cat "$HOME/intouch/recipe-prompt.md")" ------------------------------------------------------------------------------ --- macOS: ~/Library/LaunchAgents/com.intouch.recipes.plist -------------------- Label com.intouch.recipes ProgramArguments /bin/bash /Users/YOU/intouch/recipes.sh StartCalendarInterval Hour7Minute0 RunAtLoad StandardOutPath /tmp/intouch-recipes.log StandardErrorPath /tmp/intouch-recipes.log launchctl load -w ~/Library/LaunchAgents/com.intouch.recipes.plist StartCalendarInterval catches up after sleep; RunAtLoad adds a run at login. ------------------------------------------------------------------------------ --- Linux / VPS: systemd user timer ------------------------------------------ # ~/.config/systemd/user/intouch-recipes.service [Unit] Description=inTouch vocabulary set [Service] Type=oneshot ExecStart=%h/intouch/recipes.sh # ~/.config/systemd/user/intouch-recipes.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-recipes.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\recipes.ps1" $trigger = New-ScheduledTaskTrigger -Daily -At 7am $set = New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun Register-ScheduledTask -TaskName "inTouch Recipes" -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/recipes.sh ------------------------------------------------------------------------------ --- No local machine: GitHub Actions ----------------------------------------- # .github/workflows/recipes.yml (private repo, secrets for both keys) name: inTouch Recipes 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 recipes-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 recipe: missing title or invalid meal_category 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.