{
  "info": {
    "name": "Rikskampen X — Theme 5 API",
    "description": "# Rikskampen X — Theme 5 API\n\nGenerated from the verified OpenAPI documents in `docs/theme5-api/`. Those documents are the contract; this collection is only a view of them, regenerated with `python3 docs/theme5-api/postman/generate_postman.py`.\n\n## Read this first: what the API is and what is ready\n\n- **Who it is for.** This is the **staff API** of the Theme 5 CRM: coaches and admins working on their clients. It is not an API for clients (end users) of the mobile app. How clients sign in (open decision O9) is not decided, and no client-facing endpoint exists yet. A **coach** mobile app can use these endpoints; a **client** mobile app cannot yet.\n- **Contract status.** 22 operations are defined and independently reviewed:\n  - 16 core operations;\n  - 3 AI suggestion operations;\n  - 3 sign-in endpoints.\n- **Backend status.**\n  - The services, the PostgreSQL schema and store, the HTTP layer (all 22 routes) and the sign-in modules and services are implemented and verified by trusted tests, on an in-process PostgreSQL and real HTTP.\n  - The wiring of the sign-in into the running app is being finished now. Until then, sign-in is not yet served end to end by the app.\n- **Not deployed.**\n  - There is no public server or base URL yet; hosting (open decision O2) is not decided.\n  - Set `baseUrl` to the server you run, for example `http://localhost:8080`.\n  - Until the production database connection and hosting exist, a running server is a development setup.\n- **Policy gate.** The calorie floor (D4) and the activity estimate (D5) are provisional. No real client may rely on them before a qualified nutrition professional has reviewed them.\n\n## How to use the collection\n\n1. Import `Theme5-API.postman_collection.json` and `Theme5-local.postman_environment.json`, then select the environment \"Theme 5 — local\".\n2. Set `baseUrl` to your server.\n3. Run **0. Staff sign-in → Sign in a coach or admin**. Its test script stores `accessToken` in the environment. Every other request sends `Authorization: Bearer {{accessToken}}` automatically.\n4. The access token lasts 15 minutes. Run **Exchange the refresh cookie…** to get a new one. Postman keeps the HttpOnly refresh cookie for the server's domain, and a mobile client must keep it in its cookie store too. Do not store tokens in plain preferences.\n5. Writes carry `Idempotency-Key: {{$guid}}`, a new UUID per send.\n\n## Conventions every endpoint follows\n\n- **JSON** in and out. Paths start with `/api/theme5`.\n- **Errors** are `{\"code\", \"message\", \"details\"}`. `details` lists `{field, reason}` for `VALIDATION_FAILED` and `OUT_OF_BOUNDS`, and is `null` otherwise. The codes:\n\n  | Status | Codes |\n  |---|---|\n  | 400 | `VALIDATION_FAILED` |\n  | 401 | `UNAUTHENTICATED` (and `INVALID_CREDENTIALS` on sign-in) |\n  | 403 | `FORBIDDEN` (a coach not assigned to the client) |\n  | 404 | `NOT_FOUND` |\n  | 409 | `STALE_REVISION`, `DAY_LOCKED`, `IDEMPOTENCY_KEY_REUSED`, `DUPLICATE_SOURCE_EVENT`, `ALREADY_DECIDED` |\n  | 422 | `OUT_OF_BOUNDS`, `CONFIRMATION_REQUIRED` |\n  | 429 | `TOO_MANY_ATTEMPTS` (five failed sign-ins for one email within 15 minutes) |\n  | 502 | `AI_SUGGESTION_REJECTED` |\n  | 503 | `AI_UNAVAILABLE` |\n- **Idempotent writes.** Every write needs an `Idempotency-Key` of 8–128 characters.\n  - Retrying with the same key and the same request returns the stored answer, with no second write.\n  - The same key with a different request is `409 IDEMPOTENCY_KEY_REUSED`.\n- **Revisions.** Writes to a day, a meal or a recipe selection send `expectedRevision`, the revision you last read. If someone changed it in between, the answer is `409 STALE_REVISION`: read again and retry.\n- **Past days are read-only.** A day before today (Europe/Stockholm, weeks start on Monday) answers writes with `409 DAY_LOCKED`.\n- **Identifiers and units:**\n  - `clientId` matches `^[A-Za-z0-9_-]{1,64}$`;\n  - `week` is 1–52, `dayIndex` is 0–6 (the day of the program week);\n  - dates are `YYYY-MM-DD`;\n  - energy is in kcal, distance in km;\n  - an unknown value is `null`, never `0`.\n- **Audit.** Every applied write is audited and returns an `audit` stamp `{auditId, actorId, at}`.\n- **Authorization** is checked on the server for every request: a coach acts only for assigned clients, and an admin for every client. The role and the assignments are read on every request, so a change applies at once.\n",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{accessToken}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "0. Staff sign-in (auth)",
      "description": "Theme 5 staff sign-in (API03, decision D12: option B of docs/THEME5_O3_AUTHENTICATION_OPTIONS.md, chosen by the owner on 2026-10-07). Coaches and admins sign in with email and password and receive a 15-minute bearer access token (the coachAuth/adminAuth schemes of common.openapi.json) and a 12-hour refresh token in an HttpOnly cookie, rotated on every use. The token carries no role: the server reads the role and the coach-client assignments on every request. Client sign-in (O9) is not part of this contract. Coordinator-reviewed document.",
      "item": [
        {
          "name": "Sign in a coach or admin with email and password.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/auth/sign-in",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "auth",
                "sign-in"
              ]
            },
            "description": "**Sign in a coach or admin with email and password.**\n\nValidates the body (VALIDATION_FAILED), refuses after five failed sign-ins for the email within 15 minutes (TOO_MANY_ATTEMPTS), and answers INVALID_CREDENTIALS with the same message for an unknown email, an inactive account and a wrong password. Every attempt is written to the append-only authentication audit, never with the password.\n\n- operationId: `signIn`\n- Authentication: none (no bearer token)\n- Write: no\n\n**Body fields**\n- `email` (string, required; minLength 3, maxLength 254, pattern \"@\")\n- `password` (string, required; minLength 1, maxLength 256)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Signed in: a short-lived bearer access token, and the rotated refresh token in the cookie.\n- **400** — The body is not acceptable (VALIDATION_FAILED, details name the field).\n- **401** — Email or password not accepted (INVALID_CREDENTIALS).\n- **429** — Five failed sign-ins for this email within 15 minutes (TOO_MANY_ATTEMPTS).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"coach@example.com\",\n  \"password\": \"a-strong-password\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const grant = pm.response.json();",
                  "  pm.environment.set('accessToken', grant.accessToken);",
                  "  pm.environment.set('staffId', grant.staff && grant.staff.staffId);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Exchange the refresh cookie for a new access token and a rotated refresh token.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/auth/refresh",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "auth",
                "refresh"
              ]
            },
            "description": "**Exchange the refresh cookie for a new access token and a rotated refresh token.**\n\nA missing, unknown, expired or revoked refresh token, or an inactive account, is UNAUTHENTICATED and clears the cookie. Presenting an already rotated token revokes its whole family (reuse detection).\n\n- operationId: `refreshSession`\n- Authentication: none (no bearer token)\n- Write: no\n\n**Cookie**\n- `theme5_refresh` — The refresh token set by sign-in or refresh (HttpOnly, Secure, SameSite=Strict, Path=/api/theme5/auth).\n\n**Responses**\n- **200** — Signed in: a short-lived bearer access token, and the rotated refresh token in the cookie.\n- **401** — No valid refresh token (UNAUTHENTICATED); the cookie is cleared.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const grant = pm.response.json();",
                  "  pm.environment.set('accessToken', grant.accessToken);",
                  "  pm.environment.set('staffId', grant.staff && grant.staff.staffId);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Sign out: revoke the refresh token family and clear the cookie.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/auth/sign-out",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "auth",
                "sign-out"
              ]
            },
            "description": "**Sign out: revoke the refresh token family and clear the cookie.**\n\nAlways succeeds (idempotent). A known refresh token's whole family is revoked and the sign-out is audited.\n\n- operationId: `signOut`\n- Authentication: none (no bearer token)\n- Write: no\n\n**Cookie**\n- `theme5_refresh` — The refresh token set by sign-in or refresh (HttpOnly, Secure, SameSite=Strict, Path=/api/theme5/auth).\n\n**Responses**\n- **204** — Signed out; the cookie is cleared.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.environment.unset('accessToken');"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "1. Client profile and calorie profile",
      "description": "Theme 5 API contract proposal (API01, decision D11): the client profile and the derived calorie profile. The safety floor reported in the calorie profile is provisional policy (D4). Real-client gate: a qualified nutrition professional must review this provisional policy before any real client relies on it, and no production backend may apply it to real clients before that review.",
      "item": [
        {
          "name": "Get the client's recorded profile",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/profile",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "profile"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ]
            },
            "description": "**Get the client's recorded profile**\n\nThe recorded profile (canonical 3.1, including waist). A value that was never recorded is null, never 0 or a default (P03, D2); recorded values are positive numbers.\n\n- operationId: `getClientProfile`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-UNKNOWN-IS-NULL`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        },
        {
          "name": "Update recorded profile values",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/profile",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "profile"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ]
            },
            "description": "**Update recorded profile values**\n\nChange recorded profile values; sending null clears a value back to unknown. The server recomputes the calorie profile; no derived value is written.\n\n- operationId: `updateClientProfile`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-UNKNOWN-IS-NULL`, `R-DERIVED-READ-ONLY`, `R-STALE-REVISION`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Body fields**\n- `expectedRevision` (integer, required; minimum 0) — Optimistic-concurrency revision; increases by one with every applied write.\n- `age` (number/null)\n- `gender` (string/null; minLength 1)\n- `heightCm` (number/null)\n- `weightKg` (number/null)\n- `goalWeightKg` (number/null)\n- `waistCm` (number/null)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"expectedRevision\": 1,\n  \"age\": 1,\n  \"gender\": \"text\",\n  \"heightCm\": 1,\n  \"weightKg\": 1,\n  \"goalWeightKg\": 1,\n  \"waistCm\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get the derived calorie profile",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/calorie-profile",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "calorie-profile"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ]
            },
            "description": "**Get the derived calorie profile**\n\nRead-only derived values (D2, D3): Mifflin-St Jeor BMR from finite positive inputs; the activity factor from the recorded steps, with activityBasis \"steps\", or, when steps are not recorded, BMR alone with activityBasis \"bmr-only\" and activityFactor null; the goal ratio; and a recommendation clamped between the configured floor and the 1,500 kcal cap (the floor never exceeds the cap). Exercise is never added to food calories. Unavailable inputs give null values and false flags. The floor value is provisional policy (D4) and only reported here; this is read-only.\n\n- operationId: `getCalorieProfile`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-DERIVED-READ-ONLY`, `R-UNKNOWN-IS-NULL`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        }
      ]
    },
    {
      "name": "2. Calorie and meal targets",
      "description": "Theme 5 API contract proposal (API01, decision D11): day and meal targets and applying a day to the week. The below-floor acknowledgement follows the provisional safety floor policy (D4). Real-client gate: a qualified nutrition professional must review this provisional policy before any real client relies on it, and no production backend may apply it to real clients before that review.",
      "item": [
        {
          "name": "Get a day's Current and Recommended targets",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/days/:dayIndex/targets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "days",
                ":dayIndex",
                "targets"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                },
                {
                  "key": "dayIndex",
                  "value": "{{dayIndex}}",
                  "description": "Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked."
                }
              ]
            },
            "description": "**Get a day's Current and Recommended targets**\n\nThe day's Current target (the sum of its meal targets: Breakfast, Lunch and Dinner 200-750, Snack 1 150-350 and the optional Snack 2 150-350, 50-kcal steps) next to the separate, read-only Recommended target. locked is true for a day before today (Europe/Stockholm, D9).\n\n- operationId: `getDayTargets`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-CURRENT-IS-SUM`, `R-RECOMMENDED-READ-ONLY`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n- `dayIndex` — Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked. {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 6}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        },
        {
          "name": "Set a day's total and reallocate all meal targets",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/days/:dayIndex/targets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "days",
                ":dayIndex",
                "targets"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                },
                {
                  "key": "dayIndex",
                  "value": "{{dayIndex}}",
                  "description": "Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked."
                }
              ]
            },
            "description": "**Set a day's total and reallocate all meal targets**\n\nD7: after the coach confirms (confirmReplaceOverrides true; false returns 422 CONFIRMATION_REQUIRED), replaces ALL meal targets of that day, manual overrides included, with the existing proportional allocation; an unchanged total changes nothing and returns audit null. D1: an unreachable total is clamped and reported as CLAMPED_TO_REACHABLE. The server enforces 750-2,500 kcal for every coach (T06). D4: a total below the configured safety floor is applied only with acknowledgeBelowFloor true, reported as BELOW_SAFETY_FLOOR and audited; the floor policy carries a real-client gate (professional review before any real client relies on it). The Recommended target never changes. A past day returns 409 DAY_LOCKED.\n\n- operationId: `setDayTotal`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-REPLACE-ALL-AFTER-CONFIRM`, `R-PROPORTIONAL-ALLOCATION`, `R-CLAMP-AND-WARN`, `R-UNCHANGED-TOTAL-NO-OP`, `R-DAY-BOUNDS`, `R-BELOW-FLOOR-ACKNOWLEDGED`, `R-RECOMMENDED-READ-ONLY`, `R-CURRENT-IS-SUM`, `R-PAST-DAY-LOCKED`, `R-STALE-REVISION`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`, `R-MEAL-BOUNDS`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n- `dayIndex` — Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked. {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 6}\n\n**Body fields**\n- `expectedRevision` (integer, required; minimum 0) — Optimistic-concurrency revision; increases by one with every applied write.\n- `total` (integer, required; minimum 750, maximum 2500, multipleOf 50)\n- `confirmReplaceOverrides` (boolean, required)\n- `acknowledgeBelowFloor` (boolean)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"expectedRevision\": 1,\n  \"total\": 1500,\n  \"confirmReplaceOverrides\": false,\n  \"acknowledgeBelowFloor\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Set one meal's target (manual override)",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/days/:dayIndex/meals/:mealType/target",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "days",
                ":dayIndex",
                "meals",
                ":mealType",
                "target"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                },
                {
                  "key": "dayIndex",
                  "value": "{{dayIndex}}",
                  "description": "Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked."
                },
                {
                  "key": "mealType",
                  "value": "{{mealType}}",
                  "description": "Meal of the day; \"Snacks\" is shown as Snack 1."
                }
              ]
            },
            "description": "**Set one meal's target (manual override)**\n\nA manual override of one meal: the server enforces 150-350 kcal for Snacks and Snack 2 and 200-750 kcal for Breakfast, Lunch and Dinner in 50-kcal steps, and rejects an edit that would take the day above 2,500 kcal (422 OUT_OF_BOUNDS). An edit that takes the day below the configured safety floor needs acknowledgeBelowFloor true and is reported as BELOW_SAFETY_FLOOR (D4). The day's Current target follows. A past day returns 409 DAY_LOCKED.\n\n- operationId: `setMealTarget`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-MEAL-BOUNDS`, `R-DAY-BOUNDS`, `R-BELOW-FLOOR-ACKNOWLEDGED`, `R-CURRENT-IS-SUM`, `R-RECOMMENDED-READ-ONLY`, `R-PAST-DAY-LOCKED`, `R-STALE-REVISION`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n- `dayIndex` — Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked. {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 6}\n- `mealType` — Meal of the day; \"Snacks\" is shown as Snack 1. {\"$ref\": \"#/components/schemas/MealType\"}\n\n**Body fields**\n- `expectedRevision` (integer, required; minimum 0) — Optimistic-concurrency revision; increases by one with every applied write.\n- `kcal` (integer, required; minimum 150, maximum 750, multipleOf 50)\n- `acknowledgeBelowFloor` (boolean)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"expectedRevision\": 1,\n  \"kcal\": 500,\n  \"acknowledgeBelowFloor\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Apply one day's meal targets to the week",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/apply-day-targets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "apply-day-targets"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                }
              ]
            },
            "description": "**Apply one day's meal targets to the week**\n\nAfter confirmation (confirm true; false returns 422 CONFIRMATION_REQUIRED), copy one day's meal targets to the other days of the week; locked past days are skipped and reported, and the Recommended target never changes. Every changed day keeps the meal bounds (mains 200-750, snacks 150-350, 50-kcal steps), the 750-2,500 kcal day bounds and a Current target equal to the sum of its meals. Copying a day below the configured safety floor needs acknowledgeBelowFloor true and is reported as BELOW_SAFETY_FLOOR (D4).\n\n- operationId: `applyDayTargetsToWeek`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-WEEK-APPLY-SKIPS-LOCKED`, `R-WEEK-APPLY-CONFIRMED`, `R-BELOW-FLOOR-ACKNOWLEDGED`, `R-RECOMMENDED-READ-ONLY`, `R-STALE-REVISION`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`, `R-MEAL-BOUNDS`, `R-DAY-BOUNDS`, `R-CURRENT-IS-SUM`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n\n**Body fields**\n- `expectedRevision` (integer, required; minimum 0) — Optimistic-concurrency revision; increases by one with every applied write.\n- `sourceDayIndex` (integer, required; minimum 0, maximum 6)\n- `confirm` (boolean, required)\n- `acknowledgeBelowFloor` (boolean)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"expectedRevision\": 1,\n  \"sourceDayIndex\": 0,\n  \"confirm\": false,\n  \"acknowledgeBelowFloor\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "3. Recipe options and selection",
      "description": "Theme 5 API contract proposal (API01, decision D11): recipe options versus the selection, and the persisted week assignment.",
      "item": [
        {
          "name": "Get a meal's recipe options and selection",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/days/:dayIndex/meals/:mealType/recipes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "days",
                ":dayIndex",
                "meals",
                ":mealType",
                "recipes"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                },
                {
                  "key": "dayIndex",
                  "value": "{{dayIndex}}",
                  "description": "Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked."
                },
                {
                  "key": "mealType",
                  "value": "{{mealType}}",
                  "description": "Meal of the day; \"Snacks\" is shown as Snack 1."
                }
              ]
            },
            "description": "**Get a meal's recipe options and selection**\n\nAll recipe options of the meal (hidden ones flagged) and, separately, the assigned (selected) recipe ids, which never contain a hidden recipe. portionKcal is null and portionAvailable false when the portion cannot be computed from the ingredient data (PORTION).\n\n- operationId: `getMealRecipes`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-HIDDEN-NOT-ASSIGNED`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n- `dayIndex` — Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked. {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 6}\n- `mealType` — Meal of the day; \"Snacks\" is shown as Snack 1. {\"$ref\": \"#/components/schemas/MealType\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        },
        {
          "name": "Hide or show recipes for one day's meal",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/days/:dayIndex/meals/:mealType/selection",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "days",
                ":dayIndex",
                "meals",
                ":mealType",
                "selection"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                },
                {
                  "key": "dayIndex",
                  "value": "{{dayIndex}}",
                  "description": "Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked."
                },
                {
                  "key": "mealType",
                  "value": "{{mealType}}",
                  "description": "Meal of the day; \"Snacks\" is shown as Snack 1."
                }
              ]
            },
            "description": "**Hide or show recipes for one day's meal**\n\nHide or show recipes for this day's meal only: a hidden recipe leaves the selection and never appears in assignedRecipeIds; other meals, other days and the recipe library are unchanged. A past day returns 409 DAY_LOCKED.\n\n- operationId: `setMealRecipeSelection`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-HIDDEN-NOT-ASSIGNED`, `R-SELECTION-SCOPED`, `R-PAST-DAY-LOCKED`, `R-STALE-REVISION`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n- `dayIndex` — Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked. {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 6}\n- `mealType` — Meal of the day; \"Snacks\" is shown as Snack 1. {\"$ref\": \"#/components/schemas/MealType\"}\n\n**Body fields**\n- `expectedRevision` (integer, required; minimum 0) — Optimistic-concurrency revision; increases by one with every applied write.\n- `hiddenRecipeIds` (array, required)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"expectedRevision\": 1,\n  \"hiddenRecipeIds\": [\n    \"r5\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get the persisted week assignment",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/assignment",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "assignment"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                }
              ]
            },
            "description": "**Get the persisted week assignment**\n\nThe persisted week assignment: the selected recipeIds per meal and day; a hidden recipe is never included (R04, API_CONTRACT 3.1).\n\n- operationId: `getWeekAssignment`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-HIDDEN-NOT-ASSIGNED`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        }
      ]
    },
    {
      "name": "4. Activity records and summary",
      "description": "Theme 5 API contract proposal (API01, decision D11): raw activity records and the derived activity summary. The distance and calorie estimate is provisional policy (D5). Real-client gate: a qualified nutrition professional must review this provisional policy before any real client relies on it, and no production backend may apply it to real clients before that review.",
      "item": [
        {
          "name": "List raw activity records",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/activity/records?from={{fromDate}}&to={{toDate}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "activity",
                "records"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{fromDate}}",
                  "description": ""
                },
                {
                  "key": "to",
                  "value": "{{toDate}}",
                  "description": ""
                }
              ]
            },
            "description": "**List raw activity records**\n\nRaw recorded activity between the from and to dates (inclusive): one record per real source event, identified by source and sourceEventId; value is the step count, the number of stars or the workout minutes. Excluded records stay listed with excluded true and their reason. Each record appears once.\n\n- operationId: `listActivityRecords`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-RAW-APPEND-ONLY`, `R-COUNT-ONCE`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Query parameters**\n- `from` (required) —  {\"type\": \"string\", \"format\": \"date\"}\n- `to` (required) —  {\"type\": \"string\", \"format\": \"date\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        },
        {
          "name": "Append one raw activity record",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/activity/records",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "activity",
                "records"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ]
            },
            "description": "**Append one raw activity record**\n\nAppend one raw record. The same source event is stored once: a record whose source and sourceEventId already exist returns 409 DUPLICATE_SOURCE_EVENT and stores nothing; the Idempotency-Key prevents a duplicate record from a retried request. Derived summaries are never written.\n\n- operationId: `createActivityRecord`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-RAW-APPEND-ONLY`, `R-DERIVED-READ-ONLY`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`, `R-SOURCE-EVENT-ONCE`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Body fields**\n- `date` (string, required; format \"date\")\n- `kind` (string, required; enum [\"steps\", \"star\", \"workout\"])\n- `value` (integer, required; minimum 0)\n- `source` (string, required; enum [\"manual\", \"device\", \"import\"])\n- `sourceEventId` (string, required; minLength 1, maxLength 128)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **201** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"date\": \"2026-10-02\",\n  \"kind\": \"steps\",\n  \"value\": 0,\n  \"source\": \"manual\",\n  \"sourceEventId\": \"manual-0001\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Exclude a duplicate activity record",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/activity/records/:recordId/exclusion",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "activity",
                "records",
                ":recordId",
                "exclusion"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "recordId",
                  "value": "{{recordId}}",
                  "description": "Opaque raw activity record identifier."
                }
              ]
            },
            "description": "**Exclude a duplicate activity record**\n\nA coach marks one raw record excluded, with a reason of at least 10 characters; the action is audited, the raw values are never changed or deleted, and the record no longer counts in any summary, which resolves conflicts between overlapping sources (D3 revision 2). Excluding an already excluded record returns it unchanged; a stale expectedRevision returns 409 STALE_REVISION.\n\n- operationId: `excludeActivityRecord`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-EXCLUSION-AUDITED`, `R-RAW-APPEND-ONLY`, `R-OVERLAP-UNAVAILABLE`, `R-STALE-REVISION`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `recordId` — Opaque raw activity record identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Body fields**\n- `expectedRevision` (integer, required; minimum 0) — Optimistic-concurrency revision; increases by one with every applied write.\n- `reason` (string, required; minLength 10)\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — Stale expectedRevision (STALE_REVISION), a locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a source event that is already stored (DUPLICATE_SOURCE_EVENT). Nothing was changed.\n- **422** — A well-formed request-body value of the right type that breaks a value bound — minimum, exclusiveMinimum, maximum, multipleOf, minLength or maxLength (OUT_OF_BOUNDS) — or a required confirmation flag that is false (CONFIRMATION_REQUIRED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"expectedRevision\": 1,\n  \"reason\": \"Duplicate of another record\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get the derived activity summary",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/activity/summary?date={{date}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "activity",
                "summary"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ],
              "query": [
                {
                  "key": "date",
                  "value": "{{date}}",
                  "description": ""
                }
              ]
            },
            "description": "**Get the derived activity summary**\n\nDerived on request from the raw records with the provisional D5 estimate: steps and stars are the sums of that date's non-excluded records of one source, each record counted once, 1 star = 1 km, and workouts are never added to distance. When more than one source reports steps or stars for the date, that value is null and the kind is listed in conflicts (never summed twice) until a coach excludes the duplicates (excludeActivityRecord). Unavailable values are null, never 0.\n\n- operationId: `getActivitySummary`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-DERIVED-READ-ONLY`, `R-COUNT-ONCE`, `R-ESTIMATE-PROVISIONAL`, `R-UNKNOWN-IS-NULL`, `R-COACH-ASSIGNED`, `R-OVERLAP-UNAVAILABLE`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Query parameters**\n- `date` (required) —  {\"type\": \"string\", \"format\": \"date\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        }
      ]
    },
    {
      "name": "5. Audit log and permissions",
      "description": "Theme 5 API contract proposal (API01, decision D11): the audit trail and the caller's permissions.",
      "item": [
        {
          "name": "List the client's audit entries",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/audit",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "audit"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ]
            },
            "description": "**List the client's audit entries**\n\nAppend-only audit trail of every change to this client's Theme 5 data; before and after hold the previous and new values as JSON text (null when absent).\n\n- operationId: `listAuditEntries`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-AUDIT-APPEND-ONLY`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        },
        {
          "name": "Get the caller's permissions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/me/permissions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "me",
                "permissions"
              ]
            },
            "description": "**Get the caller's permissions**\n\nWho the caller is and which clients they may act on; the server enforces the same rule on every request.\n\n- operationId: `getMyPermissions`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-COACH-ASSIGNED`\n\n**Responses**\n- **200** — Success.\n- **401** — No valid session (UNAUTHENTICATED)."
          }
        }
      ]
    },
    {
      "name": "6. AI suggestions (coach review)",
      "description": "Theme 5 API contract addendum proposal (AI01, decisions D10 and D11): AI day-total suggestions that the coach reviews; client data never goes to a hosted AI provider and the AI never writes targets, meals or assignments.",
      "item": [
        {
          "name": "Request an AI day-total suggestion",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/weeks/:week/days/:dayIndex/suggestions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "weeks",
                ":week",
                "days",
                ":dayIndex",
                "suggestions"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "week",
                  "value": "{{week}}",
                  "description": "Program week, counted from the client's program start date."
                },
                {
                  "key": "dayIndex",
                  "value": "{{dayIndex}}",
                  "description": "Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked."
                }
              ]
            },
            "description": "**Request an AI day-total suggestion**\n\nAsks the self-hosted AI provider for one day calorie total for the coach to review. Only the minimum input (the Recommended and Current target, the safety floor and the day bounds) reaches the model, and client data never goes to a hosted provider (D10). Explicit backend rules check the suggestion (steps of 50, at least the safety floor, at most 2,500): a rejected suggestion returns 502 AI_SUGGESTION_REJECTED and an unavailable provider 503 AI_UNAVAILABLE, with nothing written. The request body is an empty object. The suggestion is logged and changes nothing else; a past day returns 409 DAY_LOCKED.\n\n- operationId: `requestDaySuggestion`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-AI-SELF-HOSTED`, `R-AI-MINIMUM-INPUT`, `R-AI-BACKEND-RULES`, `R-AI-COACH-DECIDES`, `R-PAST-DAY-LOCKED`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `week` — Program week, counted from the client's program start date. {\"type\": \"integer\", \"minimum\": 1, \"maximum\": 52}\n- `dayIndex` — Day of the program week, Monday = 0 to Sunday = 6 (decision D9). A day whose Europe/Stockholm date is before today is locked. {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 6}\n\n**Body fields**\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **201** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — A locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a suggestion that is already decided (ALREADY_DECIDED). Nothing was changed.\n- **502** — The AI suggestion breaks the explicit backend rules (AI_SUGGESTION_REJECTED). Nothing was changed.\n- **503** — The self-hosted AI provider is not available (AI_UNAVAILABLE). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List the client's AI suggestions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/suggestions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "suggestions"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                }
              ]
            },
            "description": "**List the client's AI suggestions**\n\nEvery AI suggestion for this client in the order it was made, with the coach's decision once it is made; read-only.\n\n- operationId: `listSuggestions`\n- Authentication: coachAuth\n- Write: no\n- Rules: `R-AI-COACH-DECIDES`, `R-COACH-ASSIGNED`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND)."
          }
        },
        {
          "name": "Decide an AI suggestion",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "A new key per write; resend the same key to retry safely (the server replays the stored answer)."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/theme5/clients/:clientId/suggestions/:suggestionId/decision",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "theme5",
                "clients",
                ":clientId",
                "suggestions",
                ":suggestionId",
                "decision"
              ],
              "variable": [
                {
                  "key": "clientId",
                  "value": "{{clientId}}",
                  "description": "Opaque client identifier."
                },
                {
                  "key": "suggestionId",
                  "value": "{{suggestionId}}",
                  "description": "Opaque AI suggestion identifier (AI addendum)."
                }
              ]
            },
            "description": "**Decide an AI suggestion**\n\nThe coach accepts or rejects a pending suggestion, and the decision is audited. Accepting applies nothing: the coach changes a day only through setDayTotal, the coach's own audited write, so no suggestion ever changes targets, meals or assignments silently. A suggestion is decided once; another decision returns 409 ALREADY_DECIDED.\n\n- operationId: `decideSuggestion`\n- Authentication: coachAuth\n- Write: yes (send an Idempotency-Key)\n- Rules: `R-AI-COACH-DECIDES`, `R-AI-DECIDED-ONCE`, `R-IDEMPOTENT-WRITE`, `R-AUDITED-WRITE`, `R-COACH-ASSIGNED`, `R-SCHEMA-400`\n\n**Path parameters**\n- `clientId` — Opaque client identifier. {\"type\": \"string\", \"pattern\": \"^[A-Za-z0-9_-]{1,64}$\"}\n- `suggestionId` — Opaque AI suggestion identifier (AI addendum). {\"type\": \"string\", \"pattern\": \"^suggestion-[0-9]+$\"}\n\n**Body fields**\n- `decision` (string, required; enum [\"accept\", \"reject\"])\n- No other properties are accepted (400 VALIDATION_FAILED).\n\n**Responses**\n- **200** — Success.\n- **400** — VALIDATION_FAILED: a path, query or header parameter that does not match its schema, or request-body structure — a missing required field, a wrong type, an unknown property, a malformed date, an unknown enum value or duplicate array items. Nothing was changed.\n- **401** — No valid session (UNAUTHENTICATED).\n- **403** — The caller may not act on this client or setting (FORBIDDEN).\n- **404** — The client, week, day or record does not exist (NOT_FOUND).\n- **409** — A locked past day (DAY_LOCKED), an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_REUSED) or a suggestion that is already decided (ALREADY_DECIDED). Nothing was changed.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"decision\": \"accept\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    }
  ]
}
