diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 2ffb877dd..8638f44b0 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -399,7 +399,7 @@ paths: "403": *a2 "409": *a7 "422": *a4 - "429": &a13 + "429": &a16 description: "Rate limit exceeded. This route shares one per-account bucket with every other authenticated write — `record-write:`, 300 requests per 60 seconds — keyed on the ACTING account, so a delegate burns their own allowance rather than the record owner's. Only the routes that meter themselves with a dedicated @@ -770,7 +770,7 @@ paths: application/json: schema: $ref: "#/components/schemas/CyclePrefsPatchEnvelope" - "400": &a15 + "400": &a18 description: The request body is not parseable JSON. Nothing was read and nothing was written. `meta.errorCode` names the surface that refused it (`.body.invalid_json`). This is a client-side serialisation fault, not a validation failure — a body that parses but fails the schema is the 422 beside this. @@ -983,7 +983,7 @@ paths: type: string format: binary "401": *a1 - "403": &a12 + "403": &a15 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Refused: this request named a record the caller may not act on (`meta.errorCode` = `sharing.access.denied`). Byte-identical for an account @@ -1309,10 +1309,38 @@ paths: schema: type: string format: binary + "400": &a10 + description: A search parameter the server cannot honour, such as an `_offset` above 9007199254740991. The body is an + `OperationOutcome` (`issue[0].code` = `invalid`, the reason in `diagnostics`) served as + `application/fhir+json`. + content: + application/fhir+json: + schema: + type: string + format: binary "401": *a1 - "403": *a9 + "403": &a11 + description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the + generic auth gate answers it before the route's own checks run. Otherwise: Module disabled for this account: + the user (or operator) has the module turned off. `meta.errorCode` = `module.disabled` and `meta.module` + carries the disabled module key (e.g. \"sleep\"). Returned even for a valid Bearer token. Clients hide the + whole module surface end-to-end rather than retry." + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a5 + "429": &a12 + description: "Rate limit exceeded: the per-account `fhir:` bucket, 120 requests per hour, shared by every + `/api/fhir/*` data route. The body is an `OperationOutcome` (`issue[0].code` = `throttled`) served as + `application/fhir+json`, not the JSON error envelope. The `Retry-After`, `X-RateLimit-Limit`, + `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers describe the bucket; back off on `Retry-After`." + headers: *a8 + content: + application/fhir+json: + schema: + type: string + format: binary /api/fhir/Observation: get: tags: @@ -1356,10 +1384,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationStatement: get: tags: @@ -1386,10 +1415,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationAdministration: get: tags: @@ -1416,10 +1446,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Coverage: get: tags: @@ -1447,10 +1478,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Patient/{id}: get: tags: @@ -1474,7 +1506,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1483,7 +1515,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Coverage/{id}: get: tags: @@ -1507,7 +1539,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1516,7 +1548,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Observation/{id}: get: tags: @@ -1541,7 +1573,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1550,7 +1582,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationStatement/{id}: get: tags: @@ -1574,7 +1606,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1583,7 +1615,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/MedicationAdministration/{id}: get: tags: @@ -1607,7 +1639,7 @@ paths: type: string format: binary "401": *a1 - "403": *a9 + "403": *a11 "404": description: OperationOutcome (application/fhir+json). content: @@ -1616,7 +1648,7 @@ paths: type: string format: binary "422": *a4 - "429": *a5 + "429": *a12 /api/fhir/Patient/$everything: get: tags: @@ -1650,10 +1682,11 @@ paths: schema: type: string format: binary + "400": *a10 "401": *a1 - "403": *a9 + "403": *a11 "422": *a4 - "429": *a5 + "429": *a12 /api/export: get: tags: @@ -2177,7 +2210,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": &a10 + "409": &a13 description: 'Refused before anything was checked or spent: the request stated `clientTransport.protocol: "http"` on a non-loopback host while this server issues `Secure` session cookies, so the browser would drop the session. `meta.errorCode` = `auth.session.insecure_transport`, `meta.sessionCookieSecure` = `true`. No password was @@ -2213,7 +2246,7 @@ paths: $ref: "#/components/schemas/MfaVerifyResponse" "401": *a1 "403": *a9 - "409": *a10 + "409": *a13 "422": *a4 "429": *a5 /api/auth/oidc/native/token: @@ -2520,7 +2553,7 @@ paths: application/json: schema: $ref: "#/components/schemas/TotpSetupEnvelope" - "401": &a11 + "401": &a14 description: No session or elevation; or, on the cookie path, `auth.reproof.required` (no recent sign-in and no proof in the body) or `auth.reproof.failed` (the proof did not verify). content: @@ -2673,7 +2706,7 @@ paths: application/json: schema: $ref: "#/components/schemas/MfaWebauthnRegisterOptionsEnvelope" - "401": *a11 + "401": *a14 "403": *a9 "422": *a4 "429": *a5 @@ -2827,7 +2860,7 @@ paths: $ref: "#/components/schemas/MfaWebauthnVerifyResponse" "401": *a1 "403": *a9 - "409": *a10 + "409": *a13 "422": *a4 "429": *a5 /api/auth/passkey/login-verify: @@ -3193,7 +3226,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": *a10 + "409": *a13 "429": *a5 /api/auth/passkeys/{id}: patch: @@ -3815,7 +3848,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": &a17 + "429": &a20 description: "Too many email-address changes, and the request asked for nothing else. The address change shares one per-account bucket across both profile routes — `profile-email:`, 10 requests per 3600 seconds — keyed on the ACTING account, so an account cannot collect a fresh allowance by probing a different address. @@ -4272,7 +4305,7 @@ paths: schema: $ref: "#/components/schemas/GetMeasurementsSeriesBatchResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/measurements/series: @@ -4327,7 +4360,7 @@ paths: schema: $ref: "#/components/schemas/GetMeasurementsSeriesResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/measurements: @@ -4548,7 +4581,7 @@ paths: schema: $ref: "#/components/schemas/ListMeasurementsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -4604,7 +4637,7 @@ paths: schema: $ref: "#/components/schemas/CreateMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": description: A measurement with this data already exists — the `(userId, type, source, externalId)` dedup index rejected the write. Never for a named sleep-stage body, which answers a re-post with 200 instead. @@ -4620,7 +4653,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/measurements/{id}: get: tags: @@ -4641,7 +4674,7 @@ paths: schema: $ref: "#/components/schemas/GetMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Measurement not found (or owned by another user). content: @@ -4689,7 +4722,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Measurement not found (or owned by another user). content: @@ -4725,7 +4758,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMeasurementResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Measurement not found (or owned by another user). content: @@ -4838,7 +4871,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementsBulkDeleteResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -4867,7 +4900,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementsRestoreResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -4900,7 +4933,7 @@ paths: schema: $ref: "#/components/schemas/SleepNightResponse" "401": *a1 - "403": &a14 + "403": &a17 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Module disabled for this account: @@ -4943,7 +4976,7 @@ paths: schema: $ref: "#/components/schemas/SleepRhythmResponse" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/workouts: @@ -4992,7 +5025,7 @@ paths: schema: $ref: "#/components/schemas/ListWorkoutsResponse" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/workouts/{id}: @@ -5028,16 +5061,7 @@ paths: schema: $ref: "#/components/schemas/GetWorkoutResponse" "401": *a1 - "403": &a21 - description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the - generic auth gate answers it before the route's own checks run. Otherwise: Module disabled for this account: - the user (or operator) has the module turned off. `meta.errorCode` = `module.disabled` and `meta.module` - carries the disabled module key (e.g. \"sleep\"). Returned even for a valid Bearer token. Clients hide the - whole module surface end-to-end rather than retry." - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorEnvelope" + "403": *a11 "404": description: Workout not found (or owned by another user). content: @@ -5189,7 +5213,7 @@ paths: schema: $ref: "#/components/schemas/ActivityCalendarEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/activity/goals: @@ -5232,7 +5256,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a13 + "429": *a16 /api/measurements/provenance: get: tags: @@ -5357,7 +5381,7 @@ paths: schema: $ref: "#/components/schemas/DayProvenanceEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/measurements/provenance/compare: @@ -5535,7 +5559,7 @@ paths: schema: $ref: "#/components/schemas/SourceComparisonEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/devices: @@ -5647,7 +5671,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeAggregateEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": description: The query failed validation — a missing or unknown `scope`, or a `days` outside 1..365. Multi-issue envelope; an audit breadcrumb is filed under the RECORD as `medications.intake.list.validation-failed` with @@ -5777,7 +5801,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/medications/{id}/glp1: get: tags: @@ -5802,7 +5826,7 @@ paths: schema: $ref: "#/components/schemas/Glp1DetailResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -5886,7 +5910,7 @@ paths: schema: $ref: "#/components/schemas/Glp1PostResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6025,7 +6049,7 @@ paths: schema: $ref: "#/components/schemas/ReminderPhaseConfigEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6143,7 +6167,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationSideEffectsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6182,7 +6206,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationSideEffectEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6227,7 +6251,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMedicationSideEffectEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No such entry for this medication and this record. An unknown id, another medication's entry and another record's entry are indistinguishable. @@ -6275,7 +6299,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMedicationIntakeEventEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No live event with that id under this medication and record. An unknown id and an already-tombstoned one are indistinguishable. @@ -6323,7 +6347,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMedicationIntakeEventEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No event with that id under this medication and record. content: @@ -6366,7 +6390,7 @@ paths: schema: $ref: "#/components/schemas/BulkDeleteMedicationIntakeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6408,7 +6432,7 @@ paths: schema: $ref: "#/components/schemas/PurgeMedicationIntakeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6453,7 +6477,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeImportAcceptedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another account). content: @@ -6534,7 +6558,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -6566,7 +6590,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -6586,7 +6610,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationCategoriesResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -6613,7 +6637,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationCategoryResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -6846,7 +6870,7 @@ paths: schema: $ref: "#/components/schemas/MedicationListLayoutResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 put: @@ -6881,7 +6905,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": &a16 + "422": &a19 description: Request validation failed. When the body carried a `baseUpdatedAt` that could not be parsed as an ISO-8601 timestamp — including an explicit `null` — `meta.errorCode` = `invalid_base_updated_at` and nothing was written. Omit the key entirely for the unconditional write; do not send `null` for it. @@ -6927,7 +6951,7 @@ paths: schema: $ref: "#/components/schemas/GetMedicationResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -6968,7 +6992,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMedicationResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7042,7 +7066,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationIntakeResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7060,7 +7084,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 get: tags: - Medications @@ -7125,7 +7149,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationIntakeEventsResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7158,7 +7182,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationInventoryResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7194,7 +7218,7 @@ paths: schema: $ref: "#/components/schemas/CreateMedicationInventoryItemResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7238,7 +7262,7 @@ paths: schema: $ref: "#/components/schemas/UpdateMedicationInventoryItemResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Inventory item not found (or owned by another user / medication). content: @@ -7273,7 +7297,7 @@ paths: schema: $ref: "#/components/schemas/DeleteMedicationInventoryItemResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Inventory item not found (or owned by another user / medication). content: @@ -7371,7 +7395,7 @@ paths: schema: $ref: "#/components/schemas/GetMedicationCadenceResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7420,7 +7444,7 @@ paths: schema: $ref: "#/components/schemas/ListMedicationComplianceResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/medications/{id}/compliance: @@ -7605,7 +7629,7 @@ paths: schema: $ref: "#/components/schemas/ListScheduleRevisionsResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7643,7 +7667,7 @@ paths: schema: $ref: "#/components/schemas/CreateScheduleRevisionResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication not found (or owned by another user). content: @@ -7687,7 +7711,7 @@ paths: schema: $ref: "#/components/schemas/UpdateScheduleRevisionResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication or revision not found (or owned by another user). content: @@ -7728,7 +7752,7 @@ paths: schema: $ref: "#/components/schemas/DeleteScheduleRevisionResponse" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Medication or revision not found (or owned by another user). content: @@ -7791,7 +7815,7 @@ paths: schema: $ref: "#/components/schemas/MedicationDoseHistoryImportQueuedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": description: A dose-history import is already in progress. content: @@ -7853,7 +7877,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeImportJobStatusEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No such job for this medication (or owned by another user). content: @@ -7887,7 +7911,7 @@ paths: schema: $ref: "#/components/schemas/MedicationIntakeImportJobStatusEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No such job for this account (or owned by another user). content: @@ -7917,7 +7941,7 @@ paths: schema: $ref: "#/components/schemas/ProfileSummaryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/ai/test: @@ -8127,7 +8151,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutCoachPrefsResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "409": @@ -8138,7 +8162,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/auth/me/disable-coach: get: @@ -8177,7 +8201,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchDisableCoachResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "422": *a4 @@ -8338,7 +8362,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchModulesResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "409": @@ -8349,7 +8373,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/auth/me/source-priority: get: @@ -9301,7 +9325,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutSourcePriorityResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "422": *a4 @@ -9461,7 +9485,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a17 + "429": *a20 /api/auth/me/notification-prefs: get: tags: @@ -9504,7 +9528,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PatchNotificationPrefsResponse" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "409": @@ -9515,7 +9539,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/auth/me/report-selection: get: @@ -9558,7 +9582,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutReportSelectionResponseEnvelope" - "400": *a15 + "400": *a18 "401": *a1 "403": *a9 "422": *a4 @@ -10352,7 +10376,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 - "503": &a20 + "503": &a23 description: "The AI capability could not be resolved (its inputs could not be read) and the answer failed closed: `meta.errorCode` = `ai.unavailable`, `meta.reason` = `check_failed`. Retry later; no data depends on it." content: @@ -10771,7 +10795,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 delete: tags: @@ -10953,7 +10977,7 @@ paths: schema: $ref: "#/components/schemas/CoachNudgeStatus" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/coach/seen: @@ -11074,7 +11098,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/anamnesis/facts: get: @@ -11093,7 +11117,7 @@ paths: schema: $ref: "#/components/schemas/GetAnamnesisFactsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -11568,7 +11592,7 @@ paths: $ref: "#/components/schemas/CorrectAnamnesisFactResponse" "401": *a1 "403": *a9 - "404": &a18 + "404": &a21 description: No current revision with this id exists for the authenticated account. content: application/json: @@ -11605,7 +11629,7 @@ paths: $ref: "#/components/schemas/RemoveAnamnesisFactResponse" "401": *a1 "403": *a9 - "404": *a18 + "404": *a21 "409": description: Anamnesis fact changed since it was loaded (optimistic-concurrency conflict). No write happened. Re-read the resource, re-apply the user's change against the fresh state, and resend with the new token. @@ -11976,7 +12000,7 @@ paths: schema: $ref: "#/components/schemas/CoachRemindersList" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -12144,7 +12168,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteList" "401": *a1 - "403": &a19 + "403": &a22 description: The session is valid but its account is not an administrator. `meta.errorCode` = `auth.admin.required`. content: application/json: @@ -12185,7 +12209,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteCreated" "401": *a1 - "403": *a19 + "403": *a22 "422": *a4 "429": *a5 /api/admin/invites/{id}: @@ -12210,7 +12234,7 @@ paths: schema: $ref: "#/components/schemas/AdminInviteRevoked" "401": *a1 - "403": *a19 + "403": *a22 "422": *a4 "429": *a5 /api/insights/chat/messages/{id}/feedback: @@ -12266,7 +12290,7 @@ paths: schema: $ref: "#/components/schemas/AdminMeasurementMaintenanceStatusResponse" "401": *a1 - "403": *a19 + "403": *a22 "422": *a4 "429": *a5 post: @@ -12396,7 +12420,7 @@ paths: schema: $ref: "#/components/schemas/DashboardSnapshotResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/comprehensive: @@ -12418,7 +12442,7 @@ paths: schema: $ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/cards: @@ -12439,7 +12463,7 @@ paths: schema: $ref: "#/components/schemas/InsightsCardsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/pregenerate: @@ -12577,7 +12601,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "503": *a20 + "503": *a23 post: tags: - Insights @@ -12783,7 +12807,7 @@ paths: schema: $ref: "#/components/schemas/CoachReadStripEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/narrative: @@ -12832,7 +12856,7 @@ paths: schema: $ref: "#/components/schemas/NarrativeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/glp1-timeline: @@ -12869,7 +12893,7 @@ paths: schema: $ref: "#/components/schemas/Glp1TimelineEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/pulse/intraday: @@ -12905,7 +12929,7 @@ paths: schema: $ref: "#/components/schemas/IntradayPulseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/settings: @@ -12981,7 +13005,7 @@ paths: schema: $ref: "#/components/schemas/TargetsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/analytics/range: @@ -13055,7 +13079,7 @@ paths: schema: $ref: "#/components/schemas/BloodPressureStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/pulse-status: @@ -13087,7 +13111,7 @@ paths: schema: $ref: "#/components/schemas/PulseStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/weight-status: @@ -13119,7 +13143,7 @@ paths: schema: $ref: "#/components/schemas/WeightStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/bmi-status: @@ -13151,7 +13175,7 @@ paths: schema: $ref: "#/components/schemas/BmiStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/mood-status: @@ -13183,7 +13207,7 @@ paths: schema: $ref: "#/components/schemas/MoodStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/medication-compliance-status: @@ -13216,7 +13240,7 @@ paths: schema: $ref: "#/components/schemas/MedicationComplianceStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/metric-status: @@ -13311,7 +13335,7 @@ paths: schema: $ref: "#/components/schemas/MetricStatusResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/biomarker-assessment: @@ -13356,7 +13380,7 @@ paths: schema: $ref: "#/components/schemas/BiomarkerAssessmentResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/derived: @@ -13468,7 +13492,7 @@ paths: schema: $ref: "#/components/schemas/DerivedMetricResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/derived/batch: @@ -13508,7 +13532,7 @@ paths: schema: $ref: "#/components/schemas/DerivedBatchResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/score-history: @@ -13558,7 +13582,7 @@ paths: schema: $ref: "#/components/schemas/ScoreHistoryResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/glp1-plateau: @@ -13578,7 +13602,7 @@ paths: schema: $ref: "#/components/schemas/InsightsGlp1PlateauResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/ecg: @@ -13600,7 +13624,7 @@ paths: schema: $ref: "#/components/schemas/EcgListResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -13678,7 +13702,7 @@ paths: schema: $ref: "#/components/schemas/EcgDetailResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No ECG recording with that id for the authenticated user (existence sealed — a foreign id is indistinguishable from a missing one). @@ -13710,7 +13734,7 @@ paths: schema: $ref: "#/components/schemas/RhythmEventsResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/correlations: @@ -13732,7 +13756,7 @@ paths: schema: $ref: "#/components/schemas/CorrelationDiscoveryResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/patterns: @@ -13750,7 +13774,7 @@ paths: schema: $ref: "#/components/schemas/CorrelationPatternListResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/patterns/{id}: @@ -13780,7 +13804,7 @@ paths: schema: $ref: "#/components/schemas/UpdateCorrelationPatternResponseEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: No current pattern with that id for this account. content: @@ -13805,7 +13829,7 @@ paths: schema: $ref: "#/components/schemas/InsightsHealthStatusEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/breathing-screening: @@ -13824,7 +13848,7 @@ paths: schema: $ref: "#/components/schemas/InsightsBreathingScreeningEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/insights/labs-changes: @@ -13843,7 +13867,7 @@ paths: schema: $ref: "#/components/schemas/InsightsLabsChangesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/mood/analytics: @@ -13866,7 +13890,7 @@ paths: schema: $ref: "#/components/schemas/MoodDailySeriesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/mood/insights: @@ -13901,7 +13925,7 @@ paths: schema: $ref: "#/components/schemas/MoodAggregatesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/mood/prognosis: @@ -13924,7 +13948,7 @@ paths: schema: $ref: "#/components/schemas/MoodPrognosisEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/mood-entries/bulk: @@ -14038,7 +14062,7 @@ paths: $ref: "#/components/schemas/MoodCustomTagPatchEnvelope" "401": *a1 "403": *a9 - "404": &a22 + "404": &a24 description: Not found / not owned by the caller. content: application/json: @@ -14074,7 +14098,7 @@ paths: $ref: "#/components/schemas/MoodCustomTagDeleteEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 /api/mood/tags/{key}/hidden: @@ -14111,7 +14135,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 /api/mood/tags/groups: @@ -14166,7 +14190,7 @@ paths: $ref: "#/components/schemas/MoodTagGroupPatchEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 delete: @@ -14198,7 +14222,7 @@ paths: $ref: "#/components/schemas/MoodTagGroupDeleteEnvelope" "401": *a1 "403": *a9 - "404": *a22 + "404": *a24 "422": *a4 "429": *a5 /api/mood/tags/layout: @@ -14248,7 +14272,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 /api/mood-entries: get: @@ -14332,7 +14356,7 @@ paths: schema: $ref: "#/components/schemas/MoodEntryListEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -14365,8 +14389,8 @@ paths: schema: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 - "403": *a12 - "409": &a23 + "403": *a15 + "409": &a25 description: A mood entry with the same `(date, moodLoggedAt)` instant already exists. `meta.errorCode` = `mood.duplicate_timestamp`. Only a request without `externalId` can answer this — with one, the write upserts in place. @@ -14375,7 +14399,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a13 + "429": *a16 /api/mood-entries/{id}: get: tags: @@ -14397,8 +14421,8 @@ paths: schema: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 - "403": *a12 - "404": *a22 + "403": *a15 + "404": *a24 "422": *a4 "429": *a5 put: @@ -14432,9 +14456,9 @@ paths: schema: $ref: "#/components/schemas/MoodEntryEnvelope" "401": *a1 - "403": *a12 - "404": *a22 - "409": *a23 + "403": *a15 + "404": *a24 + "409": *a25 "422": *a4 "429": *a5 delete: @@ -14459,8 +14483,8 @@ paths: schema: $ref: "#/components/schemas/MoodEntryDeleteEnvelope" "401": *a1 - "403": *a12 - "404": *a22 + "403": *a15 + "404": *a24 "422": *a4 "429": *a5 /api/mood-entries/restore: @@ -14489,7 +14513,7 @@ paths: schema: $ref: "#/components/schemas/MoodEntriesRestoreEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -14518,7 +14542,7 @@ paths: schema: $ref: "#/components/schemas/MoodEntriesBulkDeleteEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -14562,7 +14586,7 @@ paths: schema: $ref: "#/components/schemas/MoodLinkedContextEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/settings/ntfy: @@ -14611,7 +14635,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutNtfySettingsResponse" - "400": &a24 + "400": &a26 description: Body is not parseable JSON. content: application/json: @@ -14626,13 +14650,13 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "413": &a25 + "413": &a27 description: Body exceeds 64 KiB. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "415": &a26 + "415": &a28 description: Content-Type is not `application/json`. content: application/json: @@ -14693,7 +14717,7 @@ paths: application/json: schema: $ref: "#/components/schemas/PutTelegramSettingsResponse" - "400": *a24 + "400": *a26 "401": *a1 "403": description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the @@ -14709,8 +14733,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "413": *a25 - "415": *a26 + "413": *a27 + "415": *a28 "422": description: Body matched neither accepted shape; or `enabled` is true without both a bot token and a chat id; or the deployment's `APP_URL` is not a public HTTPS host on port 80/88/443/8443; or Telegram refused the webhook @@ -14765,11 +14789,11 @@ paths: application/json: schema: $ref: "#/components/schemas/PutWebhookSettingsResponse" - "400": *a24 + "400": *a26 "401": *a1 "403": *a9 - "413": *a25 - "415": *a26 + "413": *a27 + "415": *a28 "422": description: Body matched neither accepted shape, or `enabled` is true while no webhook URL is stored; or the URL is on a private network the operator has not listed in `NOTIFICATION_PRIVATE_ORIGINS` (`meta.errorCode` = @@ -14796,7 +14820,7 @@ paths: schema: $ref: "#/components/schemas/GetReminderThresholdsResponse" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/settings/global-services: @@ -15434,7 +15458,7 @@ paths: anyOf: - $ref: "#/components/schemas/ConsentLatestForKindEnvelope" - $ref: "#/components/schemas/ConsentLatestByKindEnvelope" - "400": &a27 + "400": &a29 description: "`kind` was present but is not one of `ai_full`, `ai_insights_only`, `ai_coach`, `ai_extraction`. Multi-issue envelope; nothing was read or revoked. The consent family answers 400 here where the rest of the API answers 422." @@ -15495,7 +15519,7 @@ paths: anyOf: - $ref: "#/components/schemas/ConsentRevokeForKindEnvelope" - $ref: "#/components/schemas/ConsentRevokeAllEnvelope" - "400": *a27 + "400": *a29 "401": *a1 "403": *a9 "422": *a4 @@ -16238,7 +16262,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderListEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -16263,7 +16287,7 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderCreatedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -16287,8 +16311,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderEnvelope" "401": *a1 - "403": *a12 - "404": &a28 + "403": *a15 + "404": &a30 description: Measurement reminder not found / not owned. content: application/json: @@ -16325,8 +16349,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderPatchEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 delete: @@ -16348,8 +16372,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderDeleteEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/satisfy: @@ -16373,8 +16397,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderSatisfyEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/complete: @@ -16401,8 +16425,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderCompleteEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/skip: @@ -16431,8 +16455,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderSkipEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/snooze: @@ -16465,8 +16489,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderSnoozeEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/measurement-reminders/{id}/history: @@ -16506,8 +16530,8 @@ paths: schema: $ref: "#/components/schemas/MeasurementReminderHistoryEnvelope" "401": *a1 - "403": *a12 - "404": *a28 + "403": *a15 + "404": *a30 "422": *a4 "429": *a5 /api/labs: @@ -16585,7 +16609,7 @@ paths: schema: $ref: "#/components/schemas/ListLabResultsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -16612,7 +16636,7 @@ paths: schema: $ref: "#/components/schemas/CreateLabResultResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": description: "Nothing was written. `meta.errorCode` = `labs.create.invalid` when the body failed validation (every issue @@ -16627,7 +16651,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/labs/overview: get: tags: @@ -16645,7 +16669,7 @@ paths: schema: $ref: "#/components/schemas/LabOverviewEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/labs/{id}: @@ -16668,8 +16692,8 @@ paths: schema: $ref: "#/components/schemas/GetLabResultResponse" "401": *a1 - "403": *a12 - "404": &a29 + "403": *a15 + "404": &a31 description: Lab result not found (or owned by another user). content: application/json: @@ -16703,8 +16727,8 @@ paths: schema: $ref: "#/components/schemas/UpdateLabResultResponse" "401": *a1 - "403": *a12 - "404": *a29 + "403": *a15 + "404": *a31 "422": *a4 "429": *a5 delete: @@ -16726,8 +16750,8 @@ paths: schema: $ref: "#/components/schemas/DeleteLabResultResponse" "401": *a1 - "403": *a12 - "404": *a29 + "403": *a15 + "404": *a31 "422": *a4 "429": *a5 /api/labs/restore: @@ -16753,7 +16777,7 @@ paths: schema: $ref: "#/components/schemas/RestoreLabResultsResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -16815,7 +16839,7 @@ paths: schema: $ref: "#/components/schemas/OcrTextExtractRequest" responses: - "202": &a39 + "202": &a41 description: Queued for the background worker. Every refusal the route can answer quickly (capability, provider, rate limit, budget, validation) has already been answered; anything that goes wrong from here is reported on the run. @@ -16939,7 +16963,7 @@ paths: schema: $ref: "#/components/schemas/ListBiomarkersEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -16964,7 +16988,7 @@ paths: schema: $ref: "#/components/schemas/CreateBiomarkerResponse" "401": *a1 - "403": *a12 + "403": *a15 "409": description: A biomarker with this name already exists for the caller. content: @@ -16972,7 +16996,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a13 + "429": *a16 /api/biomarkers/{id}: get: tags: @@ -16995,8 +17019,8 @@ paths: schema: $ref: "#/components/schemas/GetBiomarkerResponse" "401": *a1 - "403": *a12 - "404": &a30 + "403": *a15 + "404": &a32 description: "Biomarker not found (or owned by another user). `meta.errorCode` = `labs.biomarker.notFound`. Since v1.43 a marker that was merged into another answers with `meta.mergedInto` naming the marker it went into: drop the cached marker and open that one." @@ -17037,8 +17061,8 @@ paths: schema: $ref: "#/components/schemas/UpdateBiomarkerResponse" "401": *a1 - "403": *a12 - "404": *a30 + "403": *a15 + "404": *a32 "409": description: "`labs.biomarker.duplicate`: a rename collides with another of the caller's markers. `labs.biomarker.alias.taken` (v1.43): one of `aliases` is already the name or an alias of another marker; @@ -17071,8 +17095,8 @@ paths: schema: $ref: "#/components/schemas/DeleteBiomarkerResponse" "401": *a1 - "403": *a12 - "404": *a30 + "403": *a15 + "404": *a32 "422": *a4 "429": *a5 /api/biomarkers/{id}/merge: @@ -17109,8 +17133,8 @@ paths: schema: $ref: "#/components/schemas/MergeBiomarkersResponse" "401": *a1 - "403": *a12 - "404": *a30 + "403": *a15 + "404": *a32 "409": *a7 "422": description: "`labs.biomarker.merge.invalid`: a malformed body, or the target among the sources (Zod issues in @@ -17121,7 +17145,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": *a13 + "429": *a16 /api/encounters: get: tags: @@ -17184,7 +17208,7 @@ paths: schema: $ref: "#/components/schemas/ListEncountersEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17210,10 +17234,10 @@ paths: schema: $ref: "#/components/schemas/CreateEncounterEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/encounters/procedures: get: tags: @@ -17244,7 +17268,7 @@ paths: schema: $ref: "#/components/schemas/ListProceduresEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/body-sites: @@ -17281,7 +17305,7 @@ paths: schema: $ref: "#/components/schemas/ListBodySitesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/encounters/suggest: @@ -17311,7 +17335,7 @@ paths: schema: $ref: "#/components/schemas/SuggestEncounterEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/encounters/{id}: @@ -17334,8 +17358,8 @@ paths: schema: $ref: "#/components/schemas/GetEncounterEnvelope" "401": *a1 - "403": *a12 - "404": &a31 + "403": *a15 + "404": &a33 description: Visit not found (or owned by another user). content: application/json: @@ -17368,8 +17392,8 @@ paths: schema: $ref: "#/components/schemas/UpdateEncounterEnvelope" "401": *a1 - "403": *a12 - "404": *a31 + "403": *a15 + "404": *a33 "422": *a4 "429": *a5 delete: @@ -17393,8 +17417,8 @@ paths: schema: $ref: "#/components/schemas/DeleteEncounterEnvelope" "401": *a1 - "403": *a12 - "404": *a31 + "403": *a15 + "404": *a33 "422": *a4 "429": *a5 /api/encounters/{id}/restore: @@ -17419,8 +17443,8 @@ paths: schema: $ref: "#/components/schemas/RestoreEncounterEnvelope" "401": *a1 - "403": *a12 - "404": *a31 + "403": *a15 + "404": *a33 "422": *a4 "429": *a5 /api/encounters/{id}/links: @@ -17452,7 +17476,7 @@ paths: $ref: "#/components/schemas/LinkEncounterEnvelope" "401": *a1 "403": *a9 - "404": *a31 + "404": *a33 "422": *a4 "429": *a5 delete: @@ -17482,7 +17506,7 @@ paths: $ref: "#/components/schemas/UnlinkEncounterEnvelope" "401": *a1 "403": *a9 - "404": *a31 + "404": *a33 "422": *a4 "429": *a5 /api/practitioners: @@ -17522,7 +17546,7 @@ paths: schema: $ref: "#/components/schemas/ListPractitionersEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17546,7 +17570,7 @@ paths: schema: $ref: "#/components/schemas/CreatePractitionerEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -17570,8 +17594,8 @@ paths: schema: $ref: "#/components/schemas/GetPractitionerEnvelope" "401": *a1 - "403": *a12 - "404": &a32 + "403": *a15 + "404": &a34 description: Practitioner not found (or owned by another user). content: application/json: @@ -17604,8 +17628,8 @@ paths: schema: $ref: "#/components/schemas/UpdatePractitionerEnvelope" "401": *a1 - "403": *a12 - "404": *a32 + "403": *a15 + "404": *a34 "422": *a4 "429": *a5 delete: @@ -17629,8 +17653,8 @@ paths: schema: $ref: "#/components/schemas/DeletePractitionerEnvelope" "401": *a1 - "403": *a12 - "404": *a32 + "403": *a15 + "404": *a34 "422": *a4 "429": *a5 /api/practitioners/{id}/restore: @@ -17655,8 +17679,8 @@ paths: schema: $ref: "#/components/schemas/RestorePractitionerEnvelope" "401": *a1 - "403": *a12 - "404": *a32 + "403": *a15 + "404": *a34 "422": *a4 "429": *a5 /api/vaccinations/custom: @@ -17674,7 +17698,7 @@ paths: schema: $ref: "#/components/schemas/ListCustomVaccinesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17699,8 +17723,8 @@ paths: schema: $ref: "#/components/schemas/CreateCustomVaccineEnvelope" "401": *a1 - "403": *a12 - "409": &a33 + "403": *a15 + "409": &a35 description: A live definition on the record already carries this name, compared without case (`vaccination.custom.name-taken`). content: @@ -17708,7 +17732,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 - "429": *a13 + "429": *a16 /api/vaccinations/custom/{id}: patch: tags: @@ -17736,14 +17760,14 @@ paths: schema: $ref: "#/components/schemas/UpdateCustomVaccineEnvelope" "401": *a1 - "403": *a12 - "404": &a34 + "403": *a15 + "404": &a36 description: No live definition with this id on the record (`vaccination.custom.not-found`). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "409": *a33 + "409": *a35 "422": *a4 "429": *a5 delete: @@ -17767,8 +17791,8 @@ paths: schema: $ref: "#/components/schemas/DeleteCustomVaccineEnvelope" "401": *a1 - "403": *a12 - "404": *a34 + "403": *a15 + "404": *a36 "422": *a4 "429": *a5 /api/vaccinations: @@ -17821,7 +17845,7 @@ paths: schema: $ref: "#/components/schemas/ListVaccinationsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -17848,7 +17872,7 @@ paths: schema: $ref: "#/components/schemas/CreateVaccinationEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: A practice, visit or own vaccine the body names is not the record's (`vaccination.practitioner-not-found`, `vaccination.encounter-not-found`, `vaccination.custom-vaccine-not-found`). @@ -17858,7 +17882,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/vaccinations/{id}: get: tags: @@ -17879,8 +17903,8 @@ paths: schema: $ref: "#/components/schemas/GetVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": &a35 + "403": *a15 + "404": &a37 description: Vaccination not found (or owned by another user). content: application/json: @@ -17914,8 +17938,8 @@ paths: schema: $ref: "#/components/schemas/UpdateVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": *a35 + "403": *a15 + "404": *a37 "422": *a4 "429": *a5 delete: @@ -17939,8 +17963,8 @@ paths: schema: $ref: "#/components/schemas/DeleteVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": *a35 + "403": *a15 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/{id}/restore: @@ -17964,8 +17988,8 @@ paths: schema: $ref: "#/components/schemas/RestoreVaccinationEnvelope" "401": *a1 - "403": *a12 - "404": *a35 + "403": *a15 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/{id}/booster: @@ -18033,7 +18057,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "404": *a35 + "404": *a37 "422": *a4 "429": *a5 /api/vaccinations/suggest: @@ -18061,7 +18085,7 @@ paths: schema: $ref: "#/components/schemas/VaccinationSuggestEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/vaccinations/{id}/links: @@ -18094,7 +18118,7 @@ paths: $ref: "#/components/schemas/LinkVaccinationEnvelope" "401": *a1 "403": *a9 - "404": *a35 + "404": *a37 "422": *a4 "429": *a5 delete: @@ -18124,7 +18148,7 @@ paths: $ref: "#/components/schemas/UnlinkVaccinationEnvelope" "401": *a1 "403": *a9 - "404": *a35 + "404": *a37 "422": *a4 "429": *a5 /api/illness/episodes: @@ -18188,7 +18212,7 @@ paths: schema: $ref: "#/components/schemas/ListIllnessEpisodesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18214,7 +18238,7 @@ paths: schema: $ref: "#/components/schemas/CreateIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -18238,8 +18262,8 @@ paths: schema: $ref: "#/components/schemas/GetIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": &a36 + "403": *a15 + "404": &a38 description: Illness episode not found (or owned by another user). content: application/json: @@ -18273,8 +18297,8 @@ paths: schema: $ref: "#/components/schemas/UpdateIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 delete: @@ -18297,8 +18321,8 @@ paths: schema: $ref: "#/components/schemas/DeleteIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 /api/illness/episodes/{id}/restore: @@ -18325,8 +18349,8 @@ paths: schema: $ref: "#/components/schemas/RestoreIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "409": *a7 "422": *a4 "429": *a5 @@ -18358,8 +18382,8 @@ paths: schema: $ref: "#/components/schemas/ResolveIllnessEpisodeEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 /api/illness/episodes/{id}/day-logs: @@ -18409,8 +18433,8 @@ paths: schema: $ref: "#/components/schemas/GetIllnessDayLogEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 post: @@ -18446,8 +18470,8 @@ paths: schema: $ref: "#/components/schemas/UpsertIllnessDayLogCreatedEnvelope" "401": *a1 - "403": *a12 - "404": *a36 + "403": *a15 + "404": *a38 "422": *a4 "429": *a5 /api/illness/episodes/{id}/correlation: @@ -18475,7 +18499,7 @@ paths: $ref: "#/components/schemas/IllnessCorrelationEnvelope" "401": *a1 "403": *a9 - "404": *a36 + "404": *a38 "422": *a4 "429": *a5 /api/illness/insights: @@ -18549,7 +18573,7 @@ paths: schema: $ref: "#/components/schemas/ListMentalHealthAssessmentsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18582,7 +18606,7 @@ paths: schema: $ref: "#/components/schemas/CreateMentalHealthAssessmentEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -18609,7 +18633,7 @@ paths: schema: $ref: "#/components/schemas/GetMentalHealthAssessmentEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/allergies: @@ -18642,7 +18666,7 @@ paths: schema: $ref: "#/components/schemas/ListAllergiesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18667,10 +18691,10 @@ paths: schema: $ref: "#/components/schemas/CreateAllergyEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/allergies/{id}: get: tags: @@ -18691,8 +18715,8 @@ paths: schema: $ref: "#/components/schemas/GetAllergyEnvelope" "401": *a1 - "403": *a12 - "404": &a37 + "403": *a15 + "404": &a39 description: Allergy not found (or owned by another user). content: application/json: @@ -18725,8 +18749,8 @@ paths: schema: $ref: "#/components/schemas/UpdateAllergyEnvelope" "401": *a1 - "403": *a12 - "404": *a37 + "403": *a15 + "404": *a39 "422": *a4 "429": *a5 delete: @@ -18749,8 +18773,8 @@ paths: schema: $ref: "#/components/schemas/DeleteAllergyEnvelope" "401": *a1 - "403": *a12 - "404": *a37 + "403": *a15 + "404": *a39 "422": *a4 "429": *a5 /api/family-history: @@ -18774,7 +18798,7 @@ paths: schema: $ref: "#/components/schemas/ListFamilyHistoryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -18799,7 +18823,7 @@ paths: schema: $ref: "#/components/schemas/CreateFamilyHistoryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -18823,8 +18847,8 @@ paths: schema: $ref: "#/components/schemas/GetFamilyHistoryEnvelope" "401": *a1 - "403": *a12 - "404": &a38 + "403": *a15 + "404": &a40 description: Family history entry not found (or owned by another user). content: application/json: @@ -18857,8 +18881,8 @@ paths: schema: $ref: "#/components/schemas/UpdateFamilyHistoryEnvelope" "401": *a1 - "403": *a12 - "404": *a38 + "403": *a15 + "404": *a40 "422": *a4 "429": *a5 delete: @@ -18881,8 +18905,8 @@ paths: schema: $ref: "#/components/schemas/DeleteFamilyHistoryEnvelope" "401": *a1 - "403": *a12 - "404": *a38 + "403": *a15 + "404": *a40 "422": *a4 "429": *a5 /api/documents/inbound: @@ -18997,7 +19021,7 @@ paths: schema: $ref: "#/components/schemas/InboundDocumentListEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -19283,7 +19307,7 @@ paths: schema: $ref: "#/components/schemas/DocumentUsageEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/documents/inbound/layout: @@ -19301,7 +19325,7 @@ paths: schema: $ref: "#/components/schemas/DocumentsLayoutEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 put: @@ -19372,7 +19396,7 @@ paths: schema: $ref: "#/components/schemas/InboundDocumentDetailEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 patch: @@ -19494,7 +19518,7 @@ paths: type: string format: binary "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/documents/inbound/{id}/thumbnail: @@ -19523,7 +19547,7 @@ paths: type: string format: binary "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/documents/inbound/{id}/extract: @@ -19555,7 +19579,7 @@ paths: required: true schema: type: string - - &a40 + - &a42 name: Prefer in: header required: false @@ -19580,9 +19604,9 @@ paths: application/json: schema: $ref: "#/components/schemas/InboundExtractEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": &a41 + "403": &a43 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: AI refusal: the `documentAi` capability is unavailable for this record. The envelope carries `meta.errorCode`, `meta.capability` and @@ -19608,7 +19632,7 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "422": *a4 "429": *a5 - "503": &a42 + "503": &a44 description: "`ai.unavailable` — the inputs behind the AI capability could not be read, and the answer failed closed. Nothing was sent. Retry later." content: @@ -19638,7 +19662,7 @@ paths: required: true schema: type: string - - *a40 + - *a42 requestBody: required: false content: @@ -19674,12 +19698,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentSuggestEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/summary: post: tags: @@ -19729,7 +19753,7 @@ paths: type: boolean default: false description: With `persist=true`, explicitly replace an existing stored summary. - - *a40 + - *a42 requestBody: required: false content: @@ -19765,12 +19789,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentSummaryEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/index: post: tags: @@ -19796,7 +19820,7 @@ paths: required: true schema: type: string - - *a40 + - *a42 requestBody: required: false content: @@ -19832,12 +19856,12 @@ paths: application/json: schema: $ref: "#/components/schemas/DocumentIndexEnvelope" - "202": *a39 + "202": *a41 "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/chat: post: tags: @@ -19877,10 +19901,10 @@ paths: type: string description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes." "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 get: tags: - Documents @@ -19944,10 +19968,10 @@ paths: schema: $ref: "#/components/schemas/DocumentReindexEnvelope" "401": *a1 - "403": *a41 + "403": *a43 "422": *a4 "429": *a5 - "503": *a42 + "503": *a44 /api/documents/inbound/{id}/facts/{factId}: patch: tags: @@ -20031,7 +20055,7 @@ paths: schema: $ref: "#/components/schemas/ListCustomMetricsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 post: @@ -20057,7 +20081,7 @@ paths: $ref: "#/components/schemas/CreateCustomMetricResponse" "401": *a1 "403": *a9 - "409": &a44 + "409": &a46 description: A custom metric with this name already exists for the caller. content: application/json: @@ -20085,8 +20109,8 @@ paths: schema: $ref: "#/components/schemas/GetCustomMetricResponse" "401": *a1 - "403": *a12 - "404": &a43 + "403": *a15 + "404": &a45 description: Custom metric not found (or owned by another user). content: application/json: @@ -20121,8 +20145,8 @@ paths: $ref: "#/components/schemas/UpdateCustomMetricResponse" "401": *a1 "403": *a9 - "404": *a43 - "409": *a44 + "404": *a45 + "409": *a46 "422": *a4 "429": *a5 delete: @@ -20146,7 +20170,7 @@ paths: $ref: "#/components/schemas/DeleteCustomMetricResponse" "401": *a1 "403": *a9 - "404": *a43 + "404": *a45 "422": *a4 "429": *a5 /api/custom-metrics/{id}/entries: @@ -20189,8 +20213,8 @@ paths: schema: $ref: "#/components/schemas/ListCustomMetricEntriesEnvelope" "401": *a1 - "403": *a12 - "404": *a43 + "403": *a15 + "404": *a45 "422": *a4 "429": *a5 post: @@ -20220,10 +20244,10 @@ paths: $ref: "#/components/schemas/CreateCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": *a43 + "404": *a45 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/custom-metrics/{id}/entries/{entryId}: patch: tags: @@ -20257,7 +20281,7 @@ paths: $ref: "#/components/schemas/UpdateCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": &a45 + "404": &a47 description: Custom metric entry not found (or owned by another user). content: application/json: @@ -20291,7 +20315,7 @@ paths: $ref: "#/components/schemas/DeleteCustomMetricEntryResponse" "401": *a1 "403": *a9 - "404": *a45 + "404": *a47 "422": *a4 "429": *a5 /api/custom-metrics/{id}/entries/restore: @@ -20599,7 +20623,7 @@ paths: schema: $ref: "#/components/schemas/NutrientCatalogEnvelope" "401": *a1 - "403": &a46 + "403": &a48 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: The opt-in `nutrients` module is @@ -20748,7 +20772,7 @@ paths: schema: $ref: "#/components/schemas/NutrientOverviewEnvelope" "401": *a1 - "403": *a46 + "403": *a48 "422": *a4 "429": *a5 /api/nutrients/daily: @@ -20785,7 +20809,7 @@ paths: schema: $ref: "#/components/schemas/NutrientDailySeriesEnvelope" "401": *a1 - "403": *a46 + "403": *a48 "422": *a4 "429": *a5 /api/nutrients/water: @@ -20814,7 +20838,7 @@ paths: schema: $ref: "#/components/schemas/NutrientWaterWriteEnvelope" "401": *a1 - "403": *a46 + "403": *a48 "409": *a7 "422": *a4 "429": *a5 @@ -21335,7 +21359,7 @@ paths: application/json: schema: $ref: "#/components/schemas/WithingsCredentialsUpdateEnvelope" - "400": &a47 + "400": &a49 description: Body is not parseable JSON. content: application/json: @@ -21343,19 +21367,19 @@ paths: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 "403": *a9 - "413": &a48 + "413": &a50 description: Body exceeds 64 KiB. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "415": &a49 + "415": &a51 description: Content-Type is not `application/json`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": &a50 + "422": &a52 description: "`clientId` and `clientSecret` are both required and each must be 1–200 characters. The envelope carries a single fixed message, not per-field issues." content: @@ -21546,12 +21570,12 @@ paths: application/json: schema: $ref: "#/components/schemas/FitbitCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -21706,12 +21730,12 @@ paths: application/json: schema: $ref: "#/components/schemas/GoogleHealthCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -21877,12 +21901,12 @@ paths: application/json: schema: $ref: "#/components/schemas/OuraCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -21962,21 +21986,21 @@ paths: $ref: "#/components/schemas/OuraTestEnvelope" "401": *a1 "403": *a9 - "422": &a51 + "422": &a53 description: No usable connection for this provider — nothing stored, or the stored grant could no longer be resolved into an access token. `meta.errorCode` = `not_configured`. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "429": &a52 + "429": &a54 description: More than 5 requests in 60 s from this user. `meta.errorCode` = `rate_limited_self` — this is the local limiter, distinct from a `rate_limited` 502 which is the provider's. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "502": &a53 + "502": &a55 description: "The probe did not complete. `meta.errorCode` names the class: `credentials_rejected` (the provider answered 401/403 — the stored grant is gone), `rate_limited` (the PROVIDER answered 429, which is not this endpoint's own limiter), `upstream_error` (5xx), `timeout` (no answer within 5 s), `connection_failed` @@ -22022,12 +22046,12 @@ paths: application/json: schema: $ref: "#/components/schemas/PolarCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -22107,9 +22131,9 @@ paths: $ref: "#/components/schemas/PolarTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/strava/credentials: get: tags: @@ -22146,12 +22170,12 @@ paths: application/json: schema: $ref: "#/components/schemas/StravaCredentialsUpdateEnvelope" - "400": *a47 + "400": *a49 "401": *a1 "403": *a9 - "413": *a48 - "415": *a49 - "422": *a50 + "413": *a50 + "415": *a51 + "422": *a52 "429": *a5 delete: tags: @@ -22233,9 +22257,9 @@ paths: $ref: "#/components/schemas/StravaTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/status: get: tags: @@ -22273,7 +22297,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/fitbit/test: post: tags: @@ -22299,9 +22323,9 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/google-health/resume: post: tags: @@ -22319,7 +22343,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/google-health/test: post: tags: @@ -22353,9 +22377,9 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/whoop/resume: post: tags: @@ -22373,7 +22397,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/whoop/test: post: tags: @@ -22391,9 +22415,9 @@ paths: $ref: "#/components/schemas/WhoopIntegrationTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 - "502": *a53 + "422": *a53 + "429": *a54 + "502": *a55 /api/integrations/withings/resume: post: tags: @@ -22412,7 +22436,7 @@ paths: "401": *a1 "403": *a9 "422": *a4 - "429": *a52 + "429": *a54 /api/integrations/withings/test: post: tags: @@ -22431,8 +22455,8 @@ paths: $ref: "#/components/schemas/WithingsIntegrationTestEnvelope" "401": *a1 "403": *a9 - "422": *a51 - "429": *a52 + "422": *a53 + "429": *a54 "502": description: "The probe did not complete. The shared codes apply (`credentials_rejected`, `rate_limited`, `upstream_error`, `timeout`, `connection_failed`) and this path adds one of its own: @@ -22465,7 +22489,7 @@ paths: schema: $ref: "#/components/schemas/DailyDigestEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/daily/digest/dismiss: @@ -22558,7 +22582,7 @@ paths: schema: $ref: "#/components/schemas/DashboardSummaryEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/dashboard/widgets: @@ -22577,7 +22601,7 @@ paths: schema: $ref: "#/components/schemas/DashboardLayout" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 put: @@ -22612,7 +22636,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a16 + "422": *a19 "429": *a5 delete: tags: @@ -22850,7 +22874,7 @@ paths: schema: $ref: "#/components/schemas/RecordActivityEnvelope" "401": *a1 - "403": &a54 + "403": &a56 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Refused. `meta.errorCode` is `sharing.not_permitted` when the request was made while acting on another account (grant management is never @@ -22877,7 +22901,7 @@ paths: schema: $ref: "#/components/schemas/AccountGrantListEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "422": *a4 "429": *a5 post: @@ -22981,7 +23005,7 @@ paths: schema: $ref: "#/components/schemas/AcceptedAccountGrantEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "404": description: No such invitation for this account. content: @@ -23025,7 +23049,7 @@ paths: schema: $ref: "#/components/schemas/RevokedAccountGrantEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "404": description: No such grant on this account's record. content: @@ -23066,7 +23090,7 @@ paths: schema: $ref: "#/components/schemas/RenouncedAccountGrantEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "404": description: No such grant held by this account. content: @@ -23458,7 +23482,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a54 + "403": *a56 "409": description: "The record this browser session is on is not the record the request asserted. `meta.errorCode` is `sharing.session.changed`. It is not a refusal of access — the caller may well be entitled to both records — @@ -23847,7 +23871,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectionListEnvelope" "401": *a1 - "403": &a55 + "403": &a57 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: The operator has disabled the API instance-wide. Every route in this module refuses while that setting is off, for a cookie session as well as @@ -23881,7 +23905,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectionRevokeEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "404": description: No live connection with this id for this caller — also the answer for an already-revoked one, so a repeat revoke is 404 rather than an idempotent 200. @@ -23908,7 +23932,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectorTokenListEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "422": *a4 "429": *a5 post: @@ -23941,7 +23965,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "413": description: Body exceeds 16 KiB. content: @@ -23984,7 +24008,7 @@ paths: schema: $ref: "#/components/schemas/McpConnectorTokenRevokeEnvelope" "401": *a1 - "403": *a55 + "403": *a57 "404": description: No `health:read` token with this id for this caller. An already-revoked token is still found, so a repeat revoke answers 200. @@ -24011,7 +24035,7 @@ paths: schema: $ref: "#/components/schemas/RecordSettingsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/record-settings/integrations: @@ -24030,7 +24054,7 @@ paths: schema: $ref: "#/components/schemas/RecordSettingsIntegrationsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/record-settings/integrations/{provider}/connect: @@ -24046,7 +24070,7 @@ paths: record (audited). Fitbit is not offered: its classic API is retired. Cookie session, while acting on a managed record the caller guards." parameters: - - &a56 + - &a58 name: provider in: path required: true @@ -24068,7 +24092,7 @@ paths: schema: $ref: "#/components/schemas/ManagedConnectStartEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: The path named a provider outside the closed set. Answered before the caller is authenticated. content: @@ -24093,7 +24117,7 @@ paths: Audited against the record with the Guardian as the actor. Cookie session or Bearer, while acting on a managed record the caller guards." parameters: - - *a56 + - *a58 responses: "200": description: The provider is disconnected. @@ -24102,7 +24126,7 @@ paths: schema: $ref: "#/components/schemas/ManagedDisconnectEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: The path named a provider outside the closed set, or the record has no connection to it. content: @@ -24119,7 +24143,7 @@ paths: description: Returns the named family's resolved settings for the record the session is acting on. Cookie session or Bearer, while acting on a managed record the caller guards. parameters: - - &a57 + - &a59 name: family in: path required: true @@ -24143,7 +24167,7 @@ paths: schema: $ref: "#/components/schemas/GetRecordSettingsFamilyEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: Either the path named a family outside the closed set, or the record row itself is gone. The two are not distinguished, and the first is answered before the caller is authenticated at all. @@ -24163,7 +24187,7 @@ paths: ride the audit row. Body capped at 64 KiB. Cookie session or Bearer, while acting on a managed record the caller guards. parameters: - - *a57 + - *a59 requestBody: required: true content: @@ -24184,7 +24208,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: The path named a family outside the closed set. Answered before authentication. content: @@ -24298,7 +24322,7 @@ paths: schema: $ref: "#/components/schemas/ListPersonalRecordsEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": description: "`metricType` or `limit` carried an unacceptable value. `meta.errorCode` = `personal_records.invalid_query`, with the offending path under `details.issues`." @@ -24351,7 +24375,7 @@ paths: schema: $ref: "#/components/schemas/AchievementsEnvelope" "401": *a1 - "403": *a14 + "403": *a17 "422": *a4 "429": *a5 /api/environment/preferences: @@ -24377,7 +24401,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentPreferencesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment: @@ -24395,7 +24419,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentOverviewEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment/home: @@ -24432,7 +24456,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentHomeEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "413": description: Body exceeds 4 KiB. content: @@ -24492,7 +24516,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentBackfillEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: No home location is set, so there is nothing to resolve the days against (`meta.errorCode` = `environment.no_home`), or a backfill for this account is already queued in the current twenty-minute slot @@ -24552,7 +24576,7 @@ paths: schema: $ref: "#/components/schemas/EnvironmentGeocodeEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": description: "`q` was missing, empty after trimming, or longer than 120 characters. `meta.errorCode` = `environment.invalid`." @@ -24591,7 +24615,7 @@ paths: schema: $ref: "#/components/schemas/CreateEnvironmentTravelEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "413": description: Body exceeds 4 KiB. content: @@ -24644,7 +24668,7 @@ paths: schema: $ref: "#/components/schemas/DeleteEnvironmentTravelEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "404": description: No such override for this account. `meta.errorCode` = `environment.travel.not_found`. Also the answer for an id owned by somebody else — existence is not leaked. @@ -24790,13 +24814,13 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupDocument" - "403": &a58 + "403": &a60 description: Caller is not an admin. `requireAdmin()` is cookie-only, so a Bearer token lands here whatever its scope. content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "404": &a59 + "404": &a61 description: No stored copy with this id. content: application/json: @@ -24870,8 +24894,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "403": *a58 - "404": *a59 + "403": *a60 + "404": *a61 "409": description: A restore of the same account is already queued or running (`meta.errorCode` = `backup.restore.active`, `meta.jobId` names it), the stored copy was replaced by a newer one while it was being checked @@ -24915,7 +24939,7 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupRestoreJobListResponse" - "403": *a58 + "403": *a60 /api/admin/backups/restores/{jobId}: get: tags: @@ -24938,7 +24962,7 @@ paths: application/json: schema: $ref: "#/components/schemas/AdminBackupRestoreJobResponse" - "403": *a58 + "403": *a60 "404": description: No restore job with this id. content: @@ -24957,7 +24981,7 @@ paths: request leaves the server. Needs neither the Documents module nor the list: with the list unset, `available` is false and any connection still stored is listed (with `originAllowed: false`) so it can be removed. Cookie session only; not delegable." - security: &a60 + security: &a62 - cookieAuth: [] responses: "200": @@ -24967,7 +24991,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourcesStatusEnvelope" "401": *a1 - "403": &a61 + "403": &a63 description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Refused before any request to the source: `documents.sources.browserOnly` for a Bearer caller, `sharing.not_permitted` while acting on another @@ -24992,9 +25016,9 @@ paths: `documents.sources.tokenRequired`), so a stored token is never sent to an address it was not saved for. A Papra organization id is letters, digits, `_` and `-`. The token is stored encrypted (AES-256-GCM) and never returned. Ten saves and tests a minute per person. Cookie session only; not delegable." - security: *a60 + security: *a62 parameters: - - &a62 + - &a64 name: system in: path required: true @@ -25033,8 +25057,8 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceConnectionEnvelope" "401": *a1 - "403": *a61 - "404": &a63 + "403": *a63 + "404": &a65 description: "`documents.sources.unavailable`: the operator has not listed any origin in `DOCUMENT_SOURCE_ORIGINS`, so the picker is off. `documents.sources.notConnected`: no connection is saved for this system. `documents.sources.notFound`: the source has no such document, or (Papra) no such organization, including @@ -25053,7 +25077,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "502": &a64 + "502": &a66 description: "The source could not answer usefully, with the reason in `meta.errorCode` and, when the source answered at all, its HTTP status in `meta.upstreamStatus` (its body is never passed on): `documents.sources.unreachable` (no connection, timeout, or a destination refused at dial time), `documents.sources.redirected` (the source @@ -25072,9 +25096,9 @@ paths: description: Deletes the caller's connection for this system, and its token with it. Works even after the operator removed the origin from the list, and with the Documents module switched off. `disconnected` is false when there was nothing to delete. Cookie session only; not delegable. - security: *a60 + security: *a62 parameters: - - *a62 + - *a64 responses: "200": description: Removed, or there was nothing to remove. @@ -25083,7 +25107,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceDisconnectEnvelope" "401": *a1 - "403": *a61 + "403": *a63 "422": *a4 "429": *a5 /api/documents/sources/{system}/test: @@ -25094,9 +25118,9 @@ paths: description: One request to the source with the stored token. Answers `ok` with the round-trip time and stamps `lastVerifiedAt`, or the reason in `meta.errorCode`. Shares the save's allowance of ten a minute. Cookie session only; not delegable. - security: *a60 + security: *a62 parameters: - - *a62 + - *a64 responses: "200": description: The source answered. @@ -25105,9 +25129,9 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceTestEnvelope" "401": *a1 - "403": *a61 - "404": *a63 - "422": &a65 + "403": *a63 + "404": *a65 + "422": &a67 description: "`documents.sources.originNotAllowed`: the base address's origin is not on the operator's list (checked on every request, so a connection saved earlier is cut off once its origin is removed), or another validation failure." @@ -25116,7 +25140,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "502": *a64 + "502": *a66 /api/documents/sources/{system}/tags: get: tags: @@ -25124,9 +25148,9 @@ paths: summary: Tags of a connected archive description: The source's tags for the picker's filter, the first 250 by name, in the source's own ids. Shares the search allowance of sixty a minute. Cookie session only; not delegable. - security: *a60 + security: *a62 parameters: - - *a62 + - *a64 responses: "200": description: The tags. @@ -25135,11 +25159,11 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceTagsEnvelope" "401": *a1 - "403": *a61 - "404": *a63 - "422": *a65 + "403": *a63 + "404": *a65 + "422": *a67 "429": *a5 - "502": *a64 + "502": *a66 /api/documents/sources/{system}/search: get: tags: @@ -25153,9 +25177,9 @@ paths: under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` (deleted in HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared with the tag read. Cookie session only; not delegable." - security: *a60 + security: *a62 parameters: - - *a62 + - *a64 - name: q in: query required: false @@ -25196,11 +25220,11 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceSearchEnvelope" "401": *a1 - "403": *a61 - "404": *a63 - "422": *a65 + "403": *a63 + "404": *a65 + "422": *a67 "429": *a5 - "502": *a64 + "502": *a66 /api/documents/sources/{system}/import: post: tags: @@ -25219,9 +25243,9 @@ paths: fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 otherwise. Cookie session only; not delegable." - security: *a60 + security: *a62 parameters: - - *a62 + - *a64 requestBody: required: true content: @@ -25279,7 +25303,7 @@ paths: schema: $ref: "#/components/schemas/DocumentSourceImportEnvelope" "401": *a1 - "403": *a61 + "403": *a63 "404": description: "`documents.sources.unavailable`: the operator has not listed any origin in `DOCUMENT_SOURCE_ORIGINS`, so the picker is off. `documents.sources.notConnected`: no connection is saved for this system. @@ -25310,7 +25334,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "422": *a65 + "422": *a67 "429": description: "`documents.inbound.rateLimited` (the person's hourly upload allowance, with the rate-limit headers) or `documents.inbound.uploadBusy` (too many uploads in progress; `Retry-After: 1`)." @@ -25318,7 +25342,7 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" - "502": *a64 + "502": *a66 /api/symptoms/definitions: get: tags: @@ -25342,7 +25366,7 @@ paths: schema: $ref: "#/components/schemas/ListSymptomDefinitionsEnvelope" "401": *a1 - "403": &a66 + "403": &a68 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: `illness.disabled`: the record @@ -25386,7 +25410,7 @@ paths: schema: $ref: "#/components/schemas/CreateSymptomDefinitionEnvelope" "401": *a1 - "403": *a66 + "403": *a68 "409": *a7 "422": *a4 "429": *a5 @@ -25416,8 +25440,8 @@ paths: schema: $ref: "#/components/schemas/UpdateSymptomDefinitionEnvelope" "401": *a1 - "403": *a66 - "404": &a67 + "403": *a68 + "404": &a69 description: "`symptoms.definition.notFound`: no definition with this id on the record." content: application/json: @@ -25451,8 +25475,8 @@ paths: schema: $ref: "#/components/schemas/DeleteSymptomDefinitionEnvelope" "401": *a1 - "403": *a66 - "404": *a67 + "403": *a68 + "404": *a69 "422": *a4 "429": *a5 /api/symptoms/events: @@ -25501,7 +25525,7 @@ paths: schema: $ref: "#/components/schemas/ListSymptomEventsEnvelope" "401": *a1 - "403": *a66 + "403": *a68 "422": *a4 "429": *a5 post: @@ -25527,8 +25551,8 @@ paths: schema: $ref: "#/components/schemas/CreateSymptomEventEnvelope" "401": *a1 - "403": *a66 - "404": *a67 + "403": *a68 + "404": *a69 "409": *a7 "422": *a4 "429": *a5 @@ -25558,8 +25582,8 @@ paths: schema: $ref: "#/components/schemas/UpdateSymptomEventEnvelope" "401": *a1 - "403": *a66 - "404": &a68 + "403": *a68 + "404": &a70 description: "`symptoms.event.notFound`: no occurrence with this id on the record." content: application/json: @@ -25586,8 +25610,8 @@ paths: schema: $ref: "#/components/schemas/DeleteSymptomEventEnvelope" "401": *a1 - "403": *a66 - "404": *a68 + "403": *a68 + "404": *a70 "422": *a4 "429": *a5 /api/day/index: @@ -25620,7 +25644,7 @@ paths: schema: $ref: "#/components/schemas/DayIndexEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/day/notable: @@ -25655,7 +25679,7 @@ paths: schema: $ref: "#/components/schemas/DayNotableWindowEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/day/{date}: @@ -25685,7 +25709,7 @@ paths: schema: $ref: "#/components/schemas/DayEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/timeline: @@ -25738,7 +25762,7 @@ paths: schema: $ref: "#/components/schemas/TimelineEnvelope" "401": *a1 - "403": &a69 + "403": &a71 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: `module.disabled` with @@ -25777,7 +25801,7 @@ paths: schema: $ref: "#/components/schemas/TimelineReadinessEnvelope" "401": *a1 - "403": *a69 + "403": *a71 "422": *a4 "429": *a5 /api/life-events: @@ -25795,7 +25819,7 @@ paths: schema: $ref: "#/components/schemas/LifeEventListEnvelope" "401": *a1 - "403": &a70 + "403": &a72 description: >- `meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the generic auth gate answers it before the route's own checks run. Otherwise: Owner-only in v1.42: no share @@ -25837,10 +25861,10 @@ paths: schema: $ref: "#/components/schemas/CreateLifeEventEnvelope" "401": *a1 - "403": *a70 + "403": *a72 "409": *a7 "422": *a4 - "429": *a13 + "429": *a16 /api/life-events/{id}: patch: tags: @@ -25868,8 +25892,8 @@ paths: schema: $ref: "#/components/schemas/UpdateLifeEventEnvelope" "401": *a1 - "403": *a70 - "404": &a71 + "403": *a72 + "404": &a73 description: No such life event on this account. content: application/json: @@ -25896,8 +25920,8 @@ paths: schema: $ref: "#/components/schemas/DeleteLifeEventEnvelope" "401": *a1 - "403": *a70 - "404": *a71 + "403": *a72 + "404": *a73 "422": *a4 "429": *a5 /api/search: @@ -25943,7 +25967,7 @@ paths: schema: $ref: "#/components/schemas/SearchEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/auth/me/ai-usage: @@ -26072,7 +26096,7 @@ paths: required: - file responses: - "202": *a39 + "202": *a41 "401": *a1 "403": description: "`meta.errorCode` = `auth.scope.insufficient` when a Bearer token's scope does not reach this route; the @@ -26108,7 +26132,7 @@ paths: schema: $ref: "#/components/schemas/ErrorEnvelope" "429": *a5 - "503": *a20 + "503": *a23 /api/anamnesis/life-circumstances: get: tags: @@ -26123,7 +26147,7 @@ paths: schema: $ref: "#/components/schemas/LifeCircumstancesEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "422": *a4 "429": *a5 /api/anamnesis/life-circumstances/ai-sections: @@ -26212,7 +26236,7 @@ paths: schema: $ref: "#/components/schemas/AuditCCreatedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "409": *a7 "422": *a4 "429": *a5 @@ -26236,7 +26260,7 @@ paths: schema: $ref: "#/components/schemas/AuditCRemovedEnvelope" "401": *a1 - "403": *a12 + "403": *a15 "404": description: "`anamnesis.auditC.notFound`." content: @@ -26267,7 +26291,7 @@ paths: schema: $ref: "#/components/schemas/DawarichConnectEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/dawarich/status: @@ -26283,7 +26307,7 @@ paths: schema: $ref: "#/components/schemas/DawarichStatusEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/dawarich/places: @@ -26307,7 +26331,7 @@ paths: schema: $ref: "#/components/schemas/DawarichPlacesEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: "`dawarich.not_connected`." content: @@ -26345,7 +26369,7 @@ paths: schema: $ref: "#/components/schemas/DawarichSettingsEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: "`dawarich.not_connected`." content: @@ -26377,7 +26401,7 @@ paths: schema: $ref: "#/components/schemas/DawarichProbeEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 "502": @@ -26413,7 +26437,7 @@ paths: schema: $ref: "#/components/schemas/DawarichSyncEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "409": description: "`dawarich.not_connected`." content: @@ -26448,7 +26472,7 @@ paths: schema: $ref: "#/components/schemas/DawarichDisconnectEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment/travel/suggestions: @@ -26466,7 +26490,7 @@ paths: schema: $ref: "#/components/schemas/TravelSuggestionsEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "422": *a4 "429": *a5 /api/environment/travel/suggestions/{id}/dismiss: @@ -26489,7 +26513,7 @@ paths: schema: $ref: "#/components/schemas/TravelSuggestionDismissEnvelope" "401": *a1 - "403": *a21 + "403": *a11 "404": description: "`environment.travel_suggestion.not_found`." content: diff --git a/src/lib/openapi/routes/health-record.ts b/src/lib/openapi/routes/health-record.ts index 303055b89..ca515a797 100644 --- a/src/lib/openapi/routes/health-record.ts +++ b/src/lib/openapi/routes/health-record.ts @@ -15,10 +15,44 @@ import { import { dataEnvelope, errorEnvelope, + moduleDisabledResponse, recordRefusal, stdResponses, } from "./shared"; +// The FHIR face answers its own refusals as FHIR: a 400, 404 or 429 raised +// by a `/api/fhir/*` handler is an `OperationOutcome` served as +// `application/fhir+json` (`operationOutcome` in `@/lib/fhir/rest`), which is +// what a FHIR client parses on any non-2xx. The refusals that run before the +// handler's own body are the API's: the generic 401 / 403 envelopes and the +// 403 `module.disabled`, which keeps the errorCode every client already +// branches on for a disabled module. Declared here after `...stdResponses`, +// so the FHIR shapes replace the generic JSON ones. +const fhirOutcomeContent = { + "application/fhir+json": { + schema: z.string().meta({ format: "binary" }), + }, +}; + +const fhirDataRefusals = { + ...moduleDisabledResponse, + "429": { + description: + "Rate limit exceeded: the per-account `fhir:` bucket, 120 requests per hour, shared by every `/api/fhir/*` data route. The body is an `OperationOutcome` (`issue[0].code` = `throttled`) served as `application/fhir+json`, not the JSON error envelope. The `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers describe the bucket; back off on `Retry-After`.", + content: fhirOutcomeContent, + headers: stdResponses["429"].headers, + }, +}; + +const fhirSearchRefusals = { + ...fhirDataRefusals, + "400": { + description: + "A search parameter the server cannot honour, such as an `_offset` above 9007199254740991. The body is an `OperationOutcome` (`issue[0].code` = `invalid`, the reason in `diagnostics`) served as `application/fhir+json`.", + content: fhirOutcomeContent, + }, +}; + // The two request bodies this module publishes. `.meta()` CLONES in Zod 4 // rather than annotating in place, so the returned schema has to be captured // and referenced: a bare `schema.meta({...})` statement registers nothing and @@ -394,6 +428,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -426,6 +461,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -456,6 +492,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -486,6 +523,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -516,6 +554,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, @@ -546,6 +585,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -576,6 +616,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -606,6 +647,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -636,6 +678,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -666,6 +709,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirDataRefusals, }, }, }, @@ -696,6 +740,7 @@ export const healthRecordPaths: NonNullable = { }, }, ...stdResponses, + ...fhirSearchRefusals, }, }, }, diff --git a/tests/integration/routes/fhir-module-gate.test.ts b/tests/integration/routes/fhir-module-gate.test.ts index e018e8f66..fc5363f74 100644 --- a/tests/integration/routes/fhir-module-gate.test.ts +++ b/tests/integration/routes/fhir-module-gate.test.ts @@ -10,9 +10,6 @@ * record leaves; the static CapabilityStatement carries no user data and * stays reachable. The module is switched the way the settings switch it, * through the stored per-account preferences. - * - * The registry declares no 403 for these operations; the refusals below skip - * the contract check for that reason. */ import { beforeEach, describe, expect, it } from "vitest"; @@ -45,9 +42,6 @@ import { getPrismaClient, truncateAllTables } from "../setup"; const prisma = getPrismaClient(); const INSURANCE_NUMBER = "A123456789"; -const MODULE_OFF_SKIP = - "the FHIR operations declare no 403; the module gate's module.disabled envelope is undocumented on them"; - type Handler = Parameters[0]; /** Every data route, by the path a caller hits; the reads name a held id. */ @@ -135,7 +129,7 @@ async function seedOwner(doctorReport: boolean) { return user; } -async function callEach(auth: RouteAuth, skip: boolean) { +async function callEach(auth: RouteAuth) { const results = []; for (const { handler, route, params } of DATA_ROUTES) { const res = await callRoute(handler, { @@ -143,7 +137,6 @@ async function callEach(auth: RouteAuth, skip: boolean) { route, params, auth, - contract: skip ? { skip: MODULE_OFF_SKIP } : undefined, }); results.push({ route, res }); } @@ -158,7 +151,7 @@ describe("/api/fhir/* and the doctorReport module", () => { await makeToken(owner, { permissions: [FHIR_READ_SCOPE] }), await makeSession(owner), ]) { - for (const { route, res } of await callEach(auth, true)) { + for (const { route, res } of await callEach(auth)) { expect(res.status, route).toBe(403); expect(res.data, route).toBeNull(); expect(res.meta, route).toMatchObject({ @@ -177,7 +170,7 @@ describe("/api/fhir/* and the doctorReport module", () => { const owner = await seedOwner(true); const auth = await makeToken(owner, { permissions: [FHIR_READ_SCOPE] }); - for (const { route, res } of await callEach(auth, false)) { + for (const { route, res } of await callEach(auth)) { expect(res.status, route).toBe(200); expect(res.headers.get("content-type"), route).toContain( "application/fhir+json", diff --git a/tests/integration/routes/fhir.test.ts b/tests/integration/routes/fhir.test.ts index 03133023c..a094784b8 100644 --- a/tests/integration/routes/fhir.test.ts +++ b/tests/integration/routes/fhir.test.ts @@ -758,9 +758,6 @@ describe("FHIR paging", () => { route: "/api/fhir/Observation", query: { _offset: "1e20" }, auth, - contract: { - skip: "the registry declares no 400 on the FHIR searches, though the description names the 400 OperationOutcome", - }, }); expect(res.status).toBe(400); @@ -784,12 +781,11 @@ describe("FHIR paging", () => { method: "GET", route: "/api/fhir/Patient", auth, - contract: { - skip: "the registry declares the shared 429 as an application/json error envelope; the FHIR routes answer an application/fhir+json OperationOutcome", - }, }); expect(res.status).toBe(429); + expect(res.headers.get("content-type")).toContain("application/fhir+json"); + expect(Number(res.headers.get("retry-after"))).toBeGreaterThanOrEqual(1); expect(outcomeSchema.parse(res.body).issue[0].code).toBe("throttled"); expect(JSON.stringify(res.body)).not.toContain("Sample Patient"); });