BAD developer docs
Connect any agent or app to your training data. A full REST API, an MCP server with 26 tools, and badstack — a gstack-style, auto-updating skill/tool stack you install with one command. Built to be read by humans and agents alike.
BAD is the intelligence layer above your health data
BAD is not another wearable. It's the intelligence layer — the unified health graph — that sits above every wearable, lab, and health source and fuses them into one cross-source picture and one answer: “am I getting healthier, and what should I do this week?”
The API, MCP server, and badstackexist so any agent can read & write that one unified graph programmatically — activities, recovery, labs, nutrition, and body composition, merged best-of-source across providers rather than siloed per app.
Today the graph is populated by Apple Health (including Apple Watch via HealthKit) and Strava. It's designed to absorb every other source the same way — via OAuth/API adapters where they exist, or raw uploads (lab PDFs, DEXA reports, device screenshots) where they don't — each new source landing in the same canonical layers. Recovery wearables (Whoop, Oura, Garmin), biomarkers (Superpower, Function Health, TruDiagnostic), and body composition (DEXA, VO₂max) are on the roadmap via that adapter / upload pattern; they are not live yet.
Quickstart
Every request authenticates with a Bearer token. Grab a developer API key (it starts with bad_sk_) from Settings → API — that screen also hands you a ready-to-paste setup prompt for your agent. Then export it and make a call.
export BAD_API_KEY="bad_sk_..."
# Verify the key — fetch your profile
curl "https://bad.app/api/v1/me" -H "Authorization: Bearer $BAD_API_KEY"One-curl install (badstack)
The fastest path: install the badstack onto any local agent. It pulls down the skills + MCP wiring and keeps itself updated.
curl -fsSL https://bad.app/api/badstack/install | BAD_API_KEY=bad_sk_… shWhat is badstack?
badstack is a gstack-style skill and tool stack for any local coding/agent runtime. One install command drops in a curated set of BAD skills (slash commands, prompts) plus the MCP server wiring, so your agent immediately knows how to read your training data, log workouts and meals, pull your digest, and update your plan — without you hand-wiring anything.
- Auto-updating — the stack refreshes its skills + tool definitions in the background, so new BAD tools show up without a re-install.
- Agent-agnostic — works with Claude Code, Cursor, Codex, and any MCP-capable client.
- Key-scoped — everything runs against your
bad_sk_key, so it only ever touches your own account.
Your Settings → API screen gives you both the key and a copy-paste prompt that tells your agent how to use the stack.
MCP setup — per agent
The BAD MCP server is plain Streamable HTTP at https://bad.app/api/mcp. Authenticate with the same bad_sk_ key via the Authorization header. Pick your agent below.
Claude Code
claude mcp add --transport http bad https://bad.app/api/mcp \
--header "Authorization: Bearer $BAD_API_KEY"Cursor
Add to ~/.cursor/mcp.json:
{
"mcpServers": {
"bad": {
"url": "https://bad.app/api/mcp",
"headers": {
"Authorization": "Bearer bad_sk_..."
}
}
}
}Codex
Add to ~/.codex/config.toml:
[mcp_servers.bad]
url = "https://bad.app/api/mcp"
[mcp_servers.bad.headers]
Authorization = "Bearer bad_sk_..."Generic MCP client (Pi.dev / Hermes / OpenClaw / any)
Any MCP client that speaks Streamable HTTP can use this shape:
{
"mcpServers": {
"bad": {
"type": "http",
"url": "https://bad.app/api/mcp",
"headers": {
"Authorization": "Bearer bad_sk_..."
}
}
}
}Authentication
Both the REST API and the MCP server authenticate with a single header: Authorization: Bearer <token>. Two credential types are accepted:
- Developer API key — starts with
bad_sk_. Created in Settings → API. This is what agents and the badstack use. - Session token — issued to the web and iOS apps at login. Also works as a Bearer credential.
Keys are scoped to one athlete account with full read/write access. Only a SHA-256 hash and a short display prefix are stored server-side — the plaintext key is shown once, at creation. Keep it out of client-side code, and revoke it from Settings if it leaks.
{ "error": { "code": "ERROR", "message": "Invalid or revoked API key" } }MCP tools reference (26)
Every tool the BAD MCP server exposes. The user id is taken from your Bearer credential — you never pass it. * = required.
| Tool | Purpose | Params |
|---|---|---|
| get_activities | Recent workout activities. | limit? |
| get_plan_items | Training plan items for a date. | date? |
| get_training_digest | Stats, flags, and narrative summary. | type? (7day|14day|30day|weekly) |
| get_user_profile | Authenticated user profile. | — |
| log_activity | Log a manual workout. | sportType*, startTime*, localDate*, title?, durationSec?, distanceMeters? |
| log_weight | Log a body-weight reading. | weight*, slot? (morning|evening|auto), date? |
| log_habits | Log daily habit completions. | habits*, date?, notes? |
| log_daily | Write a full daily tracker row (merges). | date?, weight_in?, weight_out?, miles_run?, habits?, notes? |
| get_daily_log | A day's log (weight slots, habits, miles). | date? |
| get_daily_log_history | Daily-log history for trend analysis. | days? |
| get_timeline | Unified, deduped Strava + Apple Health timeline. | limit?, since? |
| get_activity_detail | Fused detail: sources, splits, laps, streams. | canonicalId* |
| log_meal | Log a meal with optional macros. | description*, mealType?, calories?, protein?, carbs?, fat?, fiber?, date? |
| get_nutrition | A day's macro totals + meals. | date? |
| log_measurement | Log body tape-measure data (merges). | date?, weight?, neck?, chest?, waist?, hips?, … |
| get_measurements | Body-measurement history (newest first). | limit? |
| log_lab | Log a blood / biomarker result. | displayName*, value*, unit?, date?, refLow?, refHigh?, optimalLow?, optimalHigh?, category?, notes? |
| get_labs | Latest value per biomarker + trend. | — |
| log_checkin | Subjective check-in (mood/energy/soreness/stress). | date?, mood?, energy?, soreness?, stress?, notes? |
| log_recovery | Recovery metrics (sleep, HRV, RHR, readiness). | date?, sleepMinutes?, hrv?, restingHeartRate?, readiness? |
| get_consistency | Consistency score across pillars. | days? |
| get_best_efforts | Per-distance running personal records. | — |
| get_body_fat | Latest Navy body-fat estimate + history. | limit? |
| log_body_fat | Log a Navy body-fat estimate (computes %BF). | biologicalSex*, heightInches*, neckInches*, waistInches*, hipInches?, weightLbs?, date? |
| get_memories | Durable facts the coach remembers about you. | — |
| remember | Save a durable fact for the coach to remember. | text* |
List them live at any time:
curl "https://bad.app/api/mcp" -X POST \
-H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'REST reference
All endpoints live under https://bad.app/api/v1, return JSON, and (except the auth endpoints) require the Bearer header. Grouped logically below.
Responses are canonical — one record per real-world event/day/marker, merged best-of-source across every connected provider — so reads like /timeline and /activities/detail are richer than any single source and expose which sources contributed.
Activities & Timeline
Log workouts and read a unified, deduped Strava + Apple Health history with heatmap buckets and fused per-activity detail.
| GET | /v1/activities | List activities, newest first (limit?, type?). |
| POST | /v1/activities | Log a manual activity. |
| GET | /v1/activities/{id} | A single activity by id. |
| GET | /v1/activities/detail | Fused detail for a workout (sources, splits, streams). |
| GET | /v1/activities/extras | Supplemental per-activity data. |
| GET | /v1/timeline | Unified deduped timeline + heatmap + source counts (limit?, since?). |
| GET | /v1/best-efforts | Per-distance running personal records. |
| GET | /v1/sources | Connected data sources. |
| DELETE | /v1/sources | Remove a connected data source. |
curl "https://bad.app/api/v1/timeline?limit=200" \
-H "Authorization: Bearer $BAD_API_KEY"Daily Logs & Tracker
Per-day weight slots, miles, habits, notes, subjective check-ins, the consistency score, and the reset journal.
| GET | /v1/daily-logs | Today's log, ?date=, or ?startDate=&endDate= range. |
| PATCH | /v1/daily-logs | Create / update a daily log (habits merge). |
| GET | /v1/checkin | Subjective check-in for a day. |
| POST | /v1/checkin | Log a subjective check-in (mood/energy/soreness/stress). |
| GET | /v1/consistency | Consistency score across pillars (days?). |
| GET | /v1/reset-journal | Confession / reset journal entries (limit?). |
curl "https://bad.app/api/v1/daily-logs" \
-X PATCH -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "weight_in": 178.2, "habits": { "keto": true, "core_score": 4 } }'Nutrition
Meal logging with optional AI-estimated macros and per-day totals.
| GET | /v1/nutrition | A day's macro totals + meals (date?). |
| POST | /v1/nutrition | Log a meal with optional macros. |
curl "https://bad.app/api/v1/nutrition" \
-X POST -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "description": "2 eggs + oatmeal", "calories": 420, "protein": 24 }'Health & Wellness
Navy body-fat estimates, body measurements, HealthKit sync, sync cursors, and Strava status.
| GET | /v1/body-fat | Latest body-fat estimate + history (limit?). |
| POST | /v1/body-fat | Log a Navy body-fat estimate (computes %BF). |
| GET | /v1/measurements | Body-measurement history. |
| POST | /v1/measurements | Log body measurements (merges per day). |
| DELETE | /v1/measurements | Delete a measurement row. |
| POST | /v1/sync/healthkit | Accept a HealthKit batch from iOS. |
| GET | /v1/sync/status | Per-type HealthKit sync cursors. |
| GET | /v1/strava/status | Strava connection + recent-activity count. |
| POST | /v1/strava/disconnect | Disconnect Strava. |
curl "https://bad.app/api/v1/body-fat" \
-X POST -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "biologicalSex": "male", "heightInches": 70, "neckInches": 15, "waistInches": 34, "weightLbs": 175 }'Labs
Blood / biomarker results with reference + optimal ranges and trend series.
| GET | /v1/labs | Latest value per biomarker + ranges + trend. |
| POST | /v1/labs | Log one biomarker result (upserts per marker/date). |
curl "https://bad.app/api/v1/labs" \
-X POST -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "displayName": "Total Cholesterol", "value": 168, "unit": "mg/dL", "category": "Lipids" }'Coach & Chat
Stream the AI coach, read threads, training digest, plan items, coach + messaging settings, and durable coach memories.
| POST | /v1/chat | Send a message to the AI coach (streamed response). |
| GET | /v1/chats | List chat threads. |
| GET | /v1/chats/{chatId} | A single thread with messages. |
| GET | /v1/digest | Structured training digest (type?). |
| GET | /v1/plan-items | Today's plan items (date?). |
| PATCH | /v1/plan-items/{itemId} | Update / complete a plan item. |
| GET | /v1/coach-settings | Nudge prefs + messaging readiness flags. |
| PATCH | /v1/coach-settings | Update nudge frequency + hour. |
| GET | /v1/messaging-settings | Read 1:1 messaging prefs. |
| PATCH | /v1/messaging-settings | Update phone, channel, opt-in. |
| POST | /v1/messaging/send | Send a 1:1 coach message to your own phone. |
| POST | /v1/verify-phone | Verify a phone number for messaging. |
| GET | /v1/memories | Durable facts the coach remembers. |
| POST | /v1/memories | Add a coach memory. |
| DELETE | /v1/memories | Forget a coach memory (?id=). |
curl "https://bad.app/api/v1/chat" \
-X POST -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" --no-buffer \
-d '{ "message": "How should I pace my long run Sunday?" }'Profile & Growth
Profile, preferences, badges, locker artifacts, unified search, and account deletion.
| GET | /v1/me | Authenticated user profile. |
| PATCH | /v1/profile | Update displayName, timezone, units. |
| GET | /v1/preferences | Agent preferences (tone, notes). |
| PATCH | /v1/preferences | Update agent preferences. |
| GET | /v1/badges | Earned + locked achievement badges. |
| GET | /v1/locker | Saved artifacts (plans, notes, widgets). |
| PATCH | /v1/locker/{id} | Update a locker artifact. |
| DELETE | /v1/locker/{id} | Delete a locker artifact. |
| POST | /v1/locker/reorder | Reorder locker artifacts. |
| GET | /v1/search | Unified search across activities, artifacts, chats (q=). |
| DELETE | /v1/account | Cascade-delete the user and all data. |
curl "https://bad.app/api/v1/me" \
-H "Authorization: Bearer $BAD_API_KEY"Screenshot Import
Import historical tracker rows (weight, miles, habits) from a screenshot via AI vision.
| POST | /v1/screenshot | Extract + import tracker data from an image. |
| GET | /v1/photos | List uploaded photos. |
| POST | /v1/photos | Upload a photo. |
| DELETE | /v1/photos | Delete a photo. |
curl "https://bad.app/api/v1/screenshot" \
-X POST -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "image": "data:image/png;base64,..." }'Developer / Keys
Create, list, and revoke scoped bad_sk_ API keys programmatically. The plaintext key is returned only once.
| GET | /v1/keys | List API keys (no secrets). |
| POST | /v1/keys | Create a key — returns the plaintext key ONCE. |
| DELETE | /v1/keys | Revoke a key (?id=). |
curl "https://bad.app/api/v1/keys" \
-X POST -H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Claude Code" }'Integrations & Auth
Account auth (email/password + Sign in with Apple) that mints the session tokens usable as a Bearer credential.
| POST | /v1/auth/register | Create an account → { token, user }. |
| POST | /v1/auth/login | Exchange email + password → { token, user }. |
| POST | /v1/auth/apple | Sign in with Apple → { token, user }. |
| POST | /v1/auth/session | Validate a session token → { user }. |
curl "https://bad.app/api/v1/auth/login" \
-X POST -H "Content-Type: application/json" \
-d '{ "email": "you@example.com", "password": "•••" }'Code samples
cURL
# Log a meal, then read today's macro totals
curl "https://bad.app/api/v1/nutrition" -X POST \
-H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "description": "grilled chicken + rice", "calories": 560, "protein": 48 }'
curl "https://bad.app/api/v1/nutrition" -H "Authorization: Bearer $BAD_API_KEY"JavaScript (fetch)
const BASE = 'https://bad.app/api/v1'
const headers = {
Authorization: `Bearer ${process.env.BAD_API_KEY}`,
'Content-Type': 'application/json',
}
// Read the unified timeline
const res = await fetch(`${BASE}/timeline?limit=50`, { headers })
if (!res.ok) throw new Error(await res.text())
const { activities, heatmap, counts } = await res.json()
console.log(counts, activities.length)MCP tools/call (JSON-RPC)
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 2,
"params": {
"name": "log_meal",
"arguments": {
"description": "2 eggs + oatmeal",
"calories": 420,
"protein": 24
}
}
}Over HTTP, that's a single POST to the MCP endpoint:
curl "https://bad.app/api/mcp" -X POST \
-H "Authorization: Bearer $BAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"get_training_digest","arguments":{}}}'Drop-in agent prompt
Paste this into your agent's system prompt or a project instruction file once the BAD MCP server is connected. (Settings → API generates a personalized version too.)
You have access to the BAD MCP server (the user's training + health data).
Use it proactively:
- Before advising on training, call get_training_digest and get_timeline to
ground yourself in real data.
- When the user reports a workout, meal, weight, or how they feel, log it:
log_activity / log_meal / log_weight / log_checkin / log_recovery.
- For nutrition or body-composition questions, read get_nutrition, get_body_fat,
get_measurements, and get_labs first.
- Use get_consistency to gauge adherence before pushing or easing off.
- When you learn a durable fact about the user (injury, preference, goal),
call remember so future sessions keep it.
Never ask the user for their user id — it comes from the API key. Prefer
get_timeline over get_activities for cross-source history.