# Kade Meds API Contract — Phase v0.1

**Author:** Atlas
**Consumer:** Riv (Slack bot for Kade)
**Date:** 2026-05-22
**Status:** Draft — Riv, ack this shape before writing bot code. If anything is wrong, flag now, not after the bot is built.

Companion files:
- Migration: `Team Inbox/Atlas/continuity-system/migrations/006_kade_tables.sql`
- Seed: `Team Inbox/Atlas/continuity-system/migrations/007_seed_kade_meds_and_habits.sql`

---

## Base URL & auth

**Base URL (local):** `http://localhost:3000/api/kade`
(Atlas Next.js runs on port 3000 in production via PM2 `next start`.)

**Auth:** Bearer token via `Authorization: Bearer $ATLAS_INGEST_TOKEN` — same token as the project-events endpoint. Token lives in `app/.env.local`. Riv stores it in his bot's env vars, **never inline**.

**Content-Type:** `application/json` (request + response).

---

## Endpoints

### `POST /api/kade/meds/fire`

The bot calls this **the moment it sends a medication reminder DM to Jimmie**. Logs the fire to `kade_meds_log` with `event_type='fire'`.

**Request body:**
```jsonc
{
  "med_id":         3,                        // FK to kade_meds.id (preferred — bot looks this up once via GET /today)
  "med_name":       "Adderall",               // required even when med_id given (denormalized for log survival)
  "dose_label":     "morning",                // required
  "scheduled_for":  "2026-05-23T08:00:00-05:00", // ISO 8601, required — when the dose was supposed to fire
  "channel":        "slack",                  // default: "slack"
  "external_id":    "1716470400.001234",      // Slack message ts — required for idempotency
  "notes":          null                      // optional
}
```

**Response `201 Created`:**
```jsonc
{
  "ok": true,
  "log_id": 142,
  "follow_up_at": "2026-05-23T08:30:00-05:00"  // when to fire ONE follow-up if no ack by then
}
```

**Response `200 OK` — idempotent re-post** (same `channel` + `external_id`):
```jsonc
{
  "ok": true,
  "log_id": 142,
  "deduped": true,
  "follow_up_at": "2026-05-23T08:30:00-05:00"
}
```

---

### `POST /api/kade/meds/ack`

Bot calls this when Jimmie acknowledges (👍 reaction, "done", "took it", "yep", any positive ack). Logs ack and computes `ack_minutes_late` server-side.

**Request body:**
```jsonc
{
  "fire_external_id": "1716470400.001234",    // the Slack ts of the ORIGINAL fire message — links ack to fire
  "ack_external_id":  "1716470530.005678",    // ts of the ack itself (for idempotency)
  "med_name":         "Adderall",
  "dose_label":       "morning",
  "channel":          "slack",
  "notes":            "took with coffee"      // optional, e.g. parsed from Jimmie's reply
}
```

**Response `201 Created`:**
```jsonc
{
  "ok": true,
  "log_id": 143,
  "ack_minutes_late": 2                       // server computed from the linked fire's scheduled_for
}
```

**Note on `fire_external_id`:** if Atlas can't find the original fire row, returns `200 OK` with `{ "ok": true, "log_id": N, "linked_fire": false }` and the ack is logged standalone. Bot should still treat this as success.

---

### `POST /api/kade/meds/skip`

Bot calls this when Jimmie explicitly says he skipped a dose ("skipping it", "no", "passing today"). Logs as `event_type='skip'`.

**Request body:**
```jsonc
{
  "fire_external_id": "1716470400.001234",
  "skip_external_id": "1716470900.009999",
  "med_name":         "Adderall",
  "dose_label":       "morning",
  "channel":          "slack",
  "reason":           "stomach off"           // optional, free text
}
```

**Response `201 Created`:**
```jsonc
{ "ok": true, "log_id": 144 }
```

---

### `POST /api/kade/meds/missed`

Bot calls this when the follow-up window expires with no ack and no skip. Server logs `event_type='missed'`. **No new Slack message gets sent for this** — it's just record-keeping. (Recurring-misses surface analytics happen later via the adherence endpoint.)

**Request body:**
```jsonc
{
  "fire_external_id": "1716470400.001234",
  "med_name":         "Adderall",
  "dose_label":       "morning",
  "channel":          "slack"
}
```

**Response `201 Created`:**
```jsonc
{ "ok": true, "log_id": 145 }
```

---

### `GET /api/kade/meds/today`

Bot calls this **once on startup and once per day at 00:01 local** to get the day's schedule. The bot's scheduler then fires reminders against this list.

**Request:** no body.

**Response `200 OK`:**
```jsonc
{
  "ok": true,
  "date": "2026-05-23",
  "tz": "America/Chicago",
  "doses": [
    {
      "med_id":          1,
      "name":            "Adderall",
      "dose_label":      "morning",
      "scheduled_at":    "2026-05-23T08:00:00-05:00",
      "follow_up_minutes": 30,
      "dosage_notes":    "morning dose"
    },
    {
      "med_id":          2,
      "name":            "Adderall",
      "dose_label":      "11:45am",
      "scheduled_at":    "2026-05-23T11:45:00-05:00",
      "follow_up_minutes": 30,
      "dosage_notes":    "midday dose — fixed time"
    }
    // ...
  ]
}
```

`scheduled_at` is computed server-side from `kade_meds.fire_local_time` + today's date + the server's tz. Bot doesn't have to do timezone math — just fire when `scheduled_at` arrives.

---

### `GET /api/kade/meds/adherence?days=7`

Returns adherence summary for last N days. Used by Kade voice to surface concerning trends ("you've skipped your 11:45 dose 4 of the last 7 days") — but **NOT until v0.2**. Endpoint exists in v0.1 so Riv can build against it; bot just doesn't call it yet.

**Query params:** `days` (default 7, max 90).

**Response `200 OK`:**
```jsonc
{
  "ok": true,
  "window_start": "2026-05-16",
  "window_end":   "2026-05-22",
  "summary": [
    {
      "med_name":   "Adderall",
      "dose_label": "morning",
      "fired":      7,
      "acked":      6,
      "skipped":    0,
      "missed":     1,
      "adherence_rate": 0.857,                // acked / fired
      "avg_ack_minutes_late": 4.2
    }
    // ... one row per (med_name, dose_label)
  ]
}
```

---

## Error responses (all endpoints)

### `400 Bad Request` — validation failure
```jsonc
{ "ok": false, "error": "med_name is required", "field": "med_name" }
```

### `401 Unauthorized` — bad/missing bearer token
```jsonc
{ "ok": false, "error": "unauthorized" }
```

### `429 Too Many Requests` — rate limit (unlikely at meds volume but coded defensively)
```jsonc
{ "ok": false, "error": "rate_limited", "retry_after_seconds": 30 }
```

### `500 Internal Server Error`
```jsonc
{ "ok": false, "error": "internal", "trace_id": "atlas-abc123" }
```

Riv: on any 5xx, retry with exponential backoff (1s, 2s, 4s, capped at 30s, 5 attempts). Log to bot's own error log. If still failing after 5 attempts, **send Jimmie a Slack DM in Kade's voice**: "Atlas DB is unreachable — your meds are still scheduled, I just can't log them right now. Bug me to fix it if it stays broken."

---

## Idempotency contract

**Critical for fires** — bot may crash mid-write or get killed mid-call. Repeated `/fire` with the same `external_id` returns the original `log_id`. Build the bot assuming this guarantee.

**Acks/skips/misses** are also idempotent on their own `external_id`. A bot restart that re-processes a message backlog won't create duplicate ack entries.

---

## Time zones

All `scheduled_at`, `scheduled_for`, and `occurred_at` are ISO 8601 with offset (`-05:00` for Jimmie's tz, `-06:00` when CDT shifts to CST). Server stores everything as UTC internally; the API translates on read.

**Riv: send everything to Atlas with explicit offsets.** Don't send naive local times.

---

## What's deliberately NOT in v0.1

- No task endpoints (`/api/kade/tasks/*`) — those land in v0.2 once we know how Slack DM parsing classifies "X is done" / "remind me about X at Y"
- No list endpoints (`/api/kade/lists/*`) — same
- No DND endpoints — v0.3
- No `/kade` web UI — v0.2

If Riv hits a scenario that needs one of the above before its phase, **flag it to Atlas and we discuss before adding it**. Don't expand the contract on your own — schema drift kills these projects.

---

## Sign-off

Riv: reply in Team Inbox or DM Larry with one of:
- ✅ Contract acked, will build against this
- 🔧 Change request: [what + why]
- ❓ Question: [what]

Atlas won't build the endpoints until Riv acks — no point shipping endpoints whose shape is wrong.
