GET /api/plans
Returns every published, approved Kamee training plan. Read-only.
GET https://kamee.fit/api/plans
Requires an X-Api-Key header — see Authentication.
An OpenAPI 3.1 description is available at /openapi.yaml if you want to
generate a client.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
discipline | strength | running | No | Filters the catalog. Any other value returns 400. Omit for everything. |
Any other query parameter is ignored — and, importantly, cannot be used to bypass the cache. See Caching and freshness.
Ordering
Results are sorted by discipline ascending, then title ascending. This ordering is
stable and you can rely on it. There is no pagination; you always get the full matching
set.
Response
{
generatedAt: string // ISO 8601 UTC
plans: Plan[]
}
Plan
| Field | Type | Nullable | Notes |
|---|---|---|---|
id | string | No | UUID. Stable; safe to store as your own foreign key. |
title | string | No | |
summary | string | Yes | One- or two-sentence description. |
coverUrl | string | Yes | Public image URL. null when the plan has no cover — render a fallback. |
level | string | No | One of none, beginner, intermediate, advanced. Plans without a level are reported as none, so the enum is closed and never null. |
discipline | string | No | strength or running. |
disciplineLabel | string | No | Display name for discipline: Workouts for strength, Outdoor for running. Use this rather than capitalising discipline yourself. |
weeksCount | number | No | Programme length in weeks. |
daysPerWeek | number | Yes | |
estMinutesPerSession | number | Yes | |
webUrl | string | No | https://kamee.fit/plans/{id}. Always safe to render. |
appUrl | string | No | kamee://plan/{id}. Only works where the app is installed — see Linking to plans. |
generatedAt is not a freshness signalIt is stamped when the origin builds the response, not when the response reaches you.
A CDN-served copy can carry a generatedAt roughly 25 hours old. Do not use it to decide
whether your cache is stale.
Example
{
"generatedAt": "2026-07-17T12:00:00.000Z",
"plans": [
{
"id": "3f2c8a1e-6b7d-4e9f-a1b2-c3d4e5f60718",
"title": "Couch to 5K",
"summary": "Nine weeks from the couch to your first 5K.",
"coverUrl": "https://ywkqixaobbjxdncvnqav.supabase.co/storage/v1/object/public/plan-covers/couch-to-5k.jpg",
"level": "beginner",
"discipline": "running",
"disciplineLabel": "Outdoor",
"weeksCount": 9,
"daysPerWeek": 3,
"estMinutesPerSession": 30,
"webUrl": "https://kamee.fit/plans/3f2c8a1e-6b7d-4e9f-a1b2-c3d4e5f60718",
"appUrl": "kamee://plan/3f2c8a1e-6b7d-4e9f-a1b2-c3d4e5f60718"
}
]
}
Response headers
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
Cache-Control: public, max-age=300
Vary: X-Api-Key
Status codes
| Code | Meaning |
|---|---|
200 | Success. |
400 | discipline was present but not strength or running. |
401 | Missing or wrong X-Api-Key. |
405 | Any method other than GET or OPTIONS — including HEAD. |
500 | Server-side failure. |
HEAD returns 405HEAD is explicitly rejected, so it is not a valid health check for this endpoint. Use a
GET with discipline=running if you need a cheap liveness probe — it is CDN-cached and
inexpensive.
See Errors for response bodies and handling advice.
Methods
OPTIONS returns 204. It emits CORS headers only if an origin has been configured; by
default there are none. See
Calling from a browser.