# Keep Your Tribe JSON API Built for the hermes AI agent to read the weekly digest, reason over the full people list, and log new interactions/reminders/dates on the user's behalf. All endpoints live under `/api/v1`, respond as JSON, and require an app-level API token. ## Base URL and auth ``` https://keepyourtribe.com/api/v1 ``` One header is required on every request: - `X-Api-Token: kyt_...` — the app's per-user token, generated from the **Settings** page ("API access" card). Shown once at creation; regenerate if lost. **Send the token as `X-Api-Token`, not `Authorization: Bearer ...`** — the app only reads `X-Api-Token`, and the hosting proxy may consume the `Authorization` header before the request reaches the app. Example: ``` curl -H "X-Api-Token: kyt_..." https://keepyourtribe.com/api/v1/digest ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error": "..."}` | Malformed `date` param, or a required param missing | | 401 | `{"error": "unauthorized"}` | Missing/invalid `X-Api-Token` | | 404 | `{"error": "not_found"}` | Record doesn't exist, or belongs to another user | | 422 | `{"errors": ["..."]}` | Validation failed | Dates serialize as ISO `YYYY-MM-DD` strings; timestamps as ISO 8601. ## Endpoints ### `GET /digest?date=YYYY-MM-DD` The four weekly-digest sections as of `date` (default: the user's local today). Reuses the same `DigestBuilder` as the emailed digest. ```json { "date": "2026-07-11", "overdue_people": [ /* person objects, see below */ ], "due_this_week": [ /* person objects */ ], "upcoming_important_dates": [ { "id": 1, "kind": "birthday", "label": null, "display_label": "Birthday", "occurs_on": "2026-07-20", "reminder_on": "2026-07-13", "person": { "id": 5, "display_name": "Maria (Mari)" } } ], "one_off_reminders": [ { "id": 3, "due_on": "2026-07-12", "note": "Ask about the trip", "status": "pending", "completed_at": null, "person": { "id": 5, "display_name": "Maria (Mari)" } } ] } ``` ### `GET /people?q=&archived=` All of the user's people (default: active only; pass `archived=true` for archived people), most-overdue first. `q` filters by name/nickname substring. ```json { "people": [ /* person objects */ ] } ``` ### `GET /people/:id` One person, with their **full** interaction history (vs. the last 5 on the index/digest) — useful context when drafting a check-in message. ```json { "person": { /* person object, interactions: full history */ } } ``` **Person object shape** (as returned by index/show/create/interactions, `interactions` is the last 5 entries except on `show` where it's the full history): ```json { "id": 5, "name": "Maria", "nickname": "Mari", "display_name": "Maria (Mari)", "city": "Lisbon", "country": "PT", "country_name": "Portugal", "timezone": "Lisbon", "known_from": null, "contact_channels": "...", "profile_notes": "...", "archived": false, "contact_cadence": "1_week", "contact_cadence_label": "1 week", "cadence_set_on": "2026-07-04", "snoozed_until": null, "last_interaction_at": "2026-07-04", "due_date": "2026-07-11", "due": true, "overdue": false, "days_overdue": 0, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-07-04T00:00:00Z", "important_dates": [ /* month/day/year/lead_days + next_occurrence_on/next_reminder_on */ ], "pending_one_off_reminders": [ /* due_on/note/status */ ], "interactions": [ /* occurred_at/channel/note */ ] } ``` ### `POST /people` Create a person. Body: `{"person": {"name": "...", ...}}` — same fields as the web form (`name`, `nickname`, `city`, `country`, `timezone`, `known_from`, `contact_channels`, `profile_notes`, `contact_cadence`, `already_overdue`, `archived`). `already_overdue: true` backdates the cadence so the person shows up due immediately. Returns `{"person": {...}}`, 201, or 422 with `{"errors": [...]}`. ### PATCH /people/:id Update a person. Body: `{"person": {...}}` — same fields as create (`name`, `nickname`, `city`, `country`, `timezone`, `known_from`, `contact_channels`, `profile_notes`, `contact_cadence`, `already_overdue`, `archived`). Changing `contact_cadence` restarts the cadence clock from today (and clears any snooze) — same behavior as the web form. Returns `{"person": {...}}`, 200, or 422 with `{"errors": [...]}`. ### `POST /people/:id/interactions` Log an interaction. Body: `{"interaction": {"occurred_at": "2026-07-11", "channel": "call", "note": "..."}}`. `occurred_at` defaults to the user's local today if omitted. Returns `{"interaction": {...}, "person": {...}}` (refreshed person, so the agent sees the new `due_date`), 201, or 422. ### `POST /people/:id/important_dates` Body: `{"important_date": {"kind": "birthday", "label": null, "month": 7, "day": 20, "year": 1990, "lead_days": 7}}`. `kind` is one of `birthday`, `anniversary`, `other`; `lead_days` one of `0, 1, 3, 7`. Returns `{"important_date": {...}}`, 201, or 422. ### `POST /people/:id/one_off_reminders` Body: `{"one_off_reminder": {"due_on": "2026-07-20", "note": "..."}}`. Returns `{"one_off_reminder": {...}}`, 201, or 422. ### `POST /people/:person_id/one_off_reminders/:id/done` and `/dismiss` Close out a reminder (marks it done or dismissed). Returns `{"one_off_reminder": {...}}`. ### `POST /people/:id/snooze` and `/clear_snooze` Push the person's cadence out one full cadence period from today, or clear an existing snooze. `snooze` returns 422 (`{"errors": ["Person has no contact cadence to snooze"]}`) if the person has no cadence — snoozing a no-cadence person would otherwise silently no-op. Both return `{"person": {...}}`.