Files
takeyourmeds/AGENT_API.md
T

7.9 KiB

Agent API Handbook

Externe Automatisierung (andere Agents, Cronjobs, Sprachassistenten) spricht mit TakeYourMeds über /agent/*. Diese Routen verwenden ein separates, langes Token — nicht die User-JWT-Session der PWA.

Base URL (lokal): http://localhost:8000/agent Production: https://medis.schwenk.online/agent


Mental Model: Slots + Status

Die App dreht sich um Slots aus meds.yaml. Jeder Slot steht für eine feste Einnahmezeit am Tag (z.B. morning um 08:00). Ein Slot kann genau einen Status haben:

Status Bedeutung
upcoming Zeit noch nicht erreicht
pending Zeit ist da, liegt aber noch innerhalb der reminder_window_minutes
overdue Fenster überschritten, aber noch nicht als missed gebucht
taken Genommen geloggt
missed Verpasst geloggt (oder vom 21:00-Abend-Check gesetzt)
snoozed Verschoben via /agent/log mit status: snoozed oder /api/snooze

Streak zählt nur Tage, an denen alle Slots den Status taken haben. Vergessene Slots setzen die Streak auf 0.


Authentication

Sende das Shared Secret bei jedem Request:

Authorization: Bearer <agent_token>

Konfiguriert wird der Token über die Umgebungsvariable AGENT_TOKEN (siehe .env.example). Ist AGENT_TOKEN leer, liefern alle /agent/*-Endpunkte 503 (Service Unavailable).

Situation HTTP Status Detail
Fehlender Header 401 Invalid or missing agent token
Falscher Token 401 Invalid or missing agent token
AGENT_TOKEN nicht gesetzt 503 Agent API is disabled

User JWT (Authorization: Bearer <user-token>) wird auf /agent/* nicht akzeptiert.


Endpoint Summary

Alle Pfade sind relativ zur Base URL. Alle erfordern Authorization: Bearer <agent_token>.

Method Path Zweck
GET /health Liveness: status, version, db_version
GET /status Heutige Slots + aktuelle Stats
POST /log Eine Einnahme loggen
GET /history?days=90 Heatmap-Daten der letzten N Tage
GET /config Medikamenten-Konfiguration (Slots, Zeitzone)
POST /notify Push-Benachrichtigung an alle Subscriptions senden

Request / Response Shapes

POST /log

Body (AgentLogEntry):

{
  "slot_id": "morning",
  "status": "taken",
  "day": "2026-09-22",
  "logged_at": "2026-09-22T08:05:00+02:00",
  "source": "voice"
}
Feld Pflicht Bemerkung
slot_id ja Muss in meds.yaml existieren
status nein Default: taken. Erlaubt: taken, missed, snoozed
day nein ISO-Datum. Default: heute in meds.yaml.timezone
logged_at nein ISO-Zeitstempel. Default: jetzt (UTC)
source nein Default: agent. Frei wählbar, z.B. voice, watch, cron

Response:

{
  "ok": true,
  "slot_id": "morning",
  "day": "2026-09-22",
  "status": "taken"
}

Unbekannter slot_id liefert 400 (Unknown slot).

GET /status

Response:

{
  "today": {
    "day": "2026-09-22",
    "timezone": "Europe/Berlin",
    "now": "2026-09-22T08:05:00+02:00",
    "slots": [
      {
        "id": "morning",
        "time": "08:00",
        "label": "Morgens",
        "meds": [{ "name": "Elvanse", "dose": "30mg" }],
        "status": "taken",
        "logged_at": "2026-09-22T08:05:00+02:00",
        "window_end": "2026-09-22T09:30:00+02:00",
        "snooze_until": null
      }
    ]
  },
  "stats": {/* siehe /history response */}
}

GET /history?days=90

Response:

{
  "days": [
    {
      "day": "2026-09-22",
      "level": "good",
      "slots": { "morning": "taken" }
    }
  ],
  "stats": {
    "streak": 5,
    "compliance_percent": 92.3,
    "compliance_window_days": 90,
    "days_active": 120,
    "first_day": "2026-05-25",
    "total_taken": 238,
    "milestones": [
      {
        "id": "log_50",
        "title": "Pharma-Intern: 50 Logs. Dein Arzt wäre stolz. Vielleicht.",
        "unlocked_at": "2026-07-10T..."
      }
    ]
  }
}
level Bedeutung
good Alle Slots an diesem Tag taken
bad Mindestens ein Slot missed
partial Einige Slots geloggt, aber nicht alle und kein missed
none Noch nichts geloggt

days ist auf 365 limitiert.

GET /config

Response:

{
  "timezone": "Europe/Berlin",
  "slots": [
    {
      "id": "morning",
      "time": "08:00",
      "label": "Morgens",
      "meds": [{ "name": "Elvanse", "dose": "30mg" }],
      "reminder_window_minutes": 90
    }
  ],
  "version": "1.2.0"
}

POST /notify

Body (AgentNotifyRequest):

{
  "title": "Medis",
  "body": "Elvanse warten. Dein Gehirn auch.",
  "slot_id": "morning",
  "tag": "agent-reminder"
}
Feld Pflicht Bemerkung
title ja Max. 120 Zeichen
body ja Max. 512 Zeichen
slot_id nein Wird an den Client durchgereicht
tag nein Notification-Tag zum Ersetzen älterer Notifications

Response:

{
  "ok": true,
  "sent": 2
}

sent ist die Anzahl erfolgreich versendeter Subscriptions. Fehlgeschlagene Push-Zustellungen werden still ignoriert (siehe Server-Logs).


Example Workflow

export BASE=http://localhost:8000/agent
export TOKEN=your-agent-token

# Health
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/health"

# Agent-Status abfragen
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/status"

# Genommen loggen
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slot_id":"morning","status":"taken","source":"voice"}' \
  "$BASE/log"

# History der letzten 7 Tage
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/history?days=7"

Common Errors

Code Wann
400 Unbekannter slot_id
401 Fehlender oder falscher Agent-Token
422 Ungültiger JSON-Body (z.B. status nicht erlaubt)
503 Agent-API deaktiviert (AGENT_TOKEN leer)

Rules of Thumb

  • day nicht manuell setzen, wenn du „heute“ meinst — der Server füllt es aus der konfigurierten Zeitzone.
  • status: snoozed loggt den Slot als verschoben; er taucht dann in /status als snoozed auf, bis der Status überschrieben wird.
  • /notify sendet an alle gespeicherten Push-Subscriptions. Für 1:1-Notifications brauchst du die App-Logik über den Client.
  • Für Produktvision, UI-Wording und Datenschutz: siehe SOUL.md und README.md.