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
daynicht manuell setzen, wenn du „heute“ meinst — der Server füllt es aus der konfigurierten Zeitzone.status: snoozedloggt den Slot als verschoben; er taucht dann in/statusalssnoozedauf, bis der Status überschrieben wird./notifysendet 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.mdundREADME.md.