Health exposes a versioned public OCS API.
The API is the canonical interface for all persistent Health functionality.
It is used by:
- the Health Vue frontend
- future mobile applications
- desktop applications
- command-line clients
- other Nextcloud applications
- authenticated third-party integrations
There is no separate private data API for the Health frontend.
The legacy Health application exposed API version 1.
The new Health API therefore starts with:
Health API v2
Base path:
/ocs/v2.php/apps/health/api/v2
Example:
GET /ocs/v2.php/apps/health/api/v2/entries
The first v2.php is the Nextcloud OCS protocol endpoint.
The second /v2/ is the Health API version.
All public Health API controllers must:
- extend
OCSController - return
DataResponse - use official Nextcloud authentication
- use official Nextcloud OCS exceptions
- contain explicit PHP types
- contain complete OpenAPI-compatible documentation
- be extractable by the Nextcloud OpenAPI extractor
Do not introduce custom JSON response envelopes.
For Nextcloud 33, API routes should preferably be defined using:
#[ApiRoute(...)]directly on controller methods.
Routes must be explicit and versioned.
Example concept:
#[ApiRoute(
verb: 'GET',
url: '/api/v2/entries'
)]Do not expose unversioned public Health API routes.
All Health API endpoints require an authenticated Nextcloud user unless explicitly documented otherwise.
The authenticated Nextcloud account determines ownership.
Clients must never provide an authoritative user ID.
Invalid:
{
"userId": "alice",
"metricKey": "stress",
"numericValue": 4
}The server derives the user from the authenticated request.
The Health API is designed to support external authenticated clients.
Examples:
- Android application
- iOS application
- desktop application
- CLI client
Health does not enable unrestricted CORS access for arbitrary websites.
All endpoint definitions in this document describe the payload contained in the OCS data element.
Health must not manually reproduce the OCS envelope.
Conceptually, an API consumer receives:
{
"ocs": {
"meta": {
"...": "..."
},
"data": {
"...": "Health payload"
}
}
}The outer structure is managed by Nextcloud.
Successful GET endpoints return HTTP 200.
Endpoints that create a resource return HTTP 201.
JSON should be the primary documented representation for Health clients.
Clients should request JSON using the standard OCS mechanisms.
API requests use RFC3339 timestamps with an explicit timezone or offset.
Example:
2026-08-16T14:30:00+02:00
API responses return timestamps canonically as RFC3339 UTC values using Z.
Example response timestamp:
2026-08-16T12:30:00Z
All time ranges use half-open [from, to) semantics:
fromis inclusivetois exclusive
The server must never return ambiguous local timestamps such as:
2026-08-16 14:30
without timezone information.
Database-backed resources expose numeric IDs.
Examples:
{
"id": 4711
}IDs identify resources but do not grant authorization.
A user may only access resources they own.
Health API v2 initially contains the following areas:
/modules
/configuration
/entries
/goals
/reminders
/summaries
/statistics
Modules and metric definitions are built into Health.
They are read-only through the API.
GET /api/v2/modules
Returns all modules supported by the installed Health version.
Example data:
{
"modules": [
{
"key": "stress",
"metrics": [
{
"key": "stress",
"valueType": "scale",
"minimum": 1,
"maximum": 5,
"unit": null
}
]
},
{
"key": "sleep",
"metrics": [
{
"key": "sleep_duration",
"valueType": "duration",
"unit": "minutes"
},
{
"key": "sleep_recovery",
"valueType": "scale",
"minimum": 1,
"maximum": 5,
"unit": null
}
]
}
]
}Module definitions are not user-specific.
Localized display labels do not need to form part of the stable API contract.
Stable semantic keys do.
User configuration determines which modules are enabled and where they appear.
Until persistent module configuration is implemented, Stress is initially enabled for a new user.
GET /api/v2/configuration
The response is private to the authenticated user. It contains the user's profile, the configuration for every supported metric, and private integration preferences:
{
"profile": {
"heightCm": 177.8,
"heightDisplayUnit": "in",
"dateOfBirth": "2000-02-29",
"growthReferenceSex": "female"
},
"metrics": {
"stress": {
"enabled": true,
"checkInEnabled": true,
"checkOutEnabled": false,
"displayUnit": null,
"aggregation": "average"
}
},
"searchDailyNotes": false
}PUT /api/v2/configuration
profile, metrics, and searchDailyNotes are optional. Fields not supplied retain their
current value. null explicitly clears height, dateOfBirth, and
growthReferenceSex.
{
"profile": {
"height": 70,
"heightUnit": "in",
"dateOfBirth": "2000-02-29",
"growthReferenceSex": "female"
},
"metrics": {
"weight": {
"enabled": true,
"displayUnit": "kg"
}
},
"searchDailyNotes": true
}heightUnit is cm or in; date of birth is a strict calendar YYYY-MM-DD; and
growth reference sex is female or male. The response has the same shape as
GET /api/v2/configuration; height is always returned in canonical centimetres.
The optional date and reference sex are stored for a future verified BMI-for-age
reference and do not produce a diagnosis or an inferred value.
Each returned metric configuration also contains its read-only registry
aggregation (average, count, daily, or sum). Clients must not send it
when updating preferences; it describes how Health derives summaries, including
the daily sum for Kilocalories Measurements.
Unknown metric keys, unsupported units, malformed dates, and invalid field types
return a client error. Configuration is always scoped to the authenticated user.
searchDailyNotes is a boolean and defaults to false; it is the explicit, per-user
permission for the native Nextcloud Unified Search provider to search that user's
plain-text Daily Notes.
Entries are the central Health resource.
An entry represents exactly one atomic metric value or event.
Canonical entry representation:
{
"id": 4711,
"metricKey": "stress",
"numericValue": 4,
"optionValue": null,
"context": "checkin",
"source": "web",
"recordedAt": "2026-08-16T06:30:00Z",
"createdAt": "2026-08-16T06:30:05Z",
"updatedAt": "2026-08-16T06:30:05Z",
"note": null
}Exactly one primary value representation should normally be populated.
Journal notes are plain text and have a maximum length of 1000 characters.
POST /api/v2/entries
Example numeric metric:
{
"metricKey": "stress",
"numericValue": 4,
"optionValue": null,
"context": "manual",
"source": "web",
"recordedAt": "2026-08-16T14:30:00+02:00",
"note": null
}Example event metric:
{
"metricKey": "hydration",
"numericValue": null,
"optionValue": "large_glass",
"context": "manual",
"source": "web",
"recordedAt": "2026-08-16T11:15:00+02:00",
"note": null
}The backend validates the request against the metric definition.
For the implemented Hydration metric, optionValue accepts small_glass, large_glass, coffee, cappuccino, espresso, double_espresso, latte_macchiato, cafe_au_lait, tea, or other. Coffee and tea variants remain ordinary atomic Hydration entries; they do not introduce a separate metric or endpoint. For the implemented Break metric, optionValue accepts short, regular, short_walk, long_walk, mindfulness, or fresh_air.
source accepts web, api, mobile, or notification. If omitted, it defaults to api. Source is informational metadata and never affects authentication, authorization, or ownership.
The response contains the created canonical Entry.
A single user interaction may create multiple atomic entries.
Examples:
- check-in
- check-out
- sleep recording
- future blood pressure recording
For these cases Health provides a transactional batch endpoint.
POST /api/v2/entries/batch
Example check-in:
{
"context": "checkin",
"recordedAt": "2026-08-16T08:30:00+02:00",
"entries": [
{
"metricKey": "stress",
"numericValue": 2,
"optionValue": null,
"note": null
},
{
"metricKey": "energy",
"numericValue": 4,
"optionValue": null,
"note": null
},
{
"metricKey": "mood",
"numericValue": 4,
"optionValue": null,
"note": null
}
]
}Example Sleep submission:
{
"context": "manual",
"recordedAt": "2026-08-16T07:00:00+02:00",
"entries": [
{
"metricKey": "sleep_duration",
"numericValue": 435,
"optionValue": null,
"note": null
},
{
"metricKey": "sleep_recovery",
"numericValue": 4,
"optionValue": null,
"note": null
}
]
}All entries in a batch share:
- context
- recordedAt
unless a future API version explicitly supports otherwise.
Batch creation is transactional.
Either all entries are created or none are created.
A batch contains at most 20 atomic entries.
Response:
{
"entries": [
{
"...": "canonical entry"
},
{
"...": "canonical entry"
}
]
}GET /api/v2/entries/{id}
Returns one canonical Entry.
If the entry does not exist or does not belong to the authenticated user, the endpoint must not disclose information about another user's resource.
PUT /api/v2/entries/{id}
The update request contains the complete mutable state of the entry.
Example:
{
"numericValue": 3,
"optionValue": null,
"context": "manual",
"recordedAt": "2026-08-16T14:00:00+02:00",
"note": "Project presentation"
}The metric identity of an existing Entry should not be changed through update.
The entry source is creation metadata and is not mutable through update.
To change:
stress
into:
energy
the client should delete the original entry and create another one.
Response contains the updated canonical Entry.
DELETE /api/v2/entries/{id}
Successful deletion returns:
null
in the OCS data field.
The endpoint must be idempotent only if explicitly implemented that way.
The API must not expose whether an inaccessible ID belongs to another user.
GET /api/v2/entries
Entries are returned newest first.
Default ordering:
recordedAt DESC
id DESC
The secondary ID ordering guarantees deterministic pagination when multiple entries share the same timestamp.
The list endpoint should support:
metricKey
context
from
to
limit
cursor
Example:
GET /api/v2/entries?metricKey=stress&from=2026-08-01T00:00:00%2B02:00&limit=50
from and to filter by:
recordedAt
not createdAt.
The range uses [from, to) semantics: entries at from are included and entries at to are excluded.
Health uses cursor-based pagination for journal entries.
Do not expose database offsets as the preferred pagination mechanism.
Default page size:
50
Maximum page size:
200
Example first request:
GET /api/v2/entries?limit=50
Example data:
{
"entries": [
{
"...": "..."
}
],
"nextCursor": "opaque-value"
}Next request:
GET /api/v2/entries?limit=50&cursor=opaque-value
The cursor must be treated as opaque by clients.
The implementation may internally encode information such as:
recordedAt + id
but that representation is not part of the public API contract.
When no additional page exists:
{
"entries": [],
"nextCursor": null
}or, if the current page contains final entries:
{
"entries": [
{
"...": "..."
}
],
"nextCursor": null
}Goals are personal targets associated with a metric.
Canonical representation:
{
"id": 81,
"metricKey": "hydration",
"goalType": "minimum_count",
"targetValue": 6,
"period": "day",
"enabled": true
}Initial goal types:
minimum_count
minimum_value
maximum_value
completion
Initial period:
day
GET /api/v2/goals
Response:
{
"goals": []
}POST /api/v2/goals
Example:
{
"metricKey": "hydration",
"goalType": "minimum_count",
"targetValue": 6,
"period": "day",
"enabled": true
}PUT /api/v2/goals/{id}
Response contains the resulting Goal.
DELETE /api/v2/goals/{id}
GET /api/v2/goals/progress
Successful deletion returns:
null
Reminders describe user-configured Nextcloud notification schedules.
Canonical example:
{
"id": 31,
"moduleKey": "hydration",
"reminderType": "tracking",
"localTime": "11:00",
"enabled": true
}The API stores reminder configuration.
Delivery itself is performed through official Nextcloud notification mechanisms.
GET /api/v2/reminders
POST /api/v2/reminders
Example:
{
"moduleKey": "hydration",
"reminderType": "tracking",
"localTime": "11:00",
"enabled": true
}localTime represents a local wall-clock time in the user's configured Nextcloud timezone.
PUT /api/v2/reminders/{id}
DELETE /api/v2/reminders/{id}
Summaries are calculated resources.
They are not persisted as separate rows in the MVP.
The API provides a general daily summary endpoint.
GET /api/v2/summaries/days
Supported query parameters:
from
to
Example:
GET /api/v2/summaries/days?from=2026-08-10&to=2026-08-16
Conceptual daily result:
{
"date": "2026-08-16",
"metrics": {
"stress": {
"average": 2.8,
"count": 3
},
"hydration": {
"count": 6
},
"break": {
"count": 3
},
"sleep_duration": {
"average": 435
}
},
"goals": {
"total": 3,
"completed": 2
},
"checkinCompleted": true,
"checkoutCompleted": true
}Only values meaningful to a metric should be populated.
Do not fabricate meaningless statistics.
The daily-summary resource remains available for clients that need daily goal and journal summaries:
GET /api/v2/summaries/days?from=&to=
The from and to parameters define the requested period using [from, to) semantics. It does not define a separate Overview screen in Health v3.
Health v3 Statistics returns day-level, owner-scoped data for one or more requested chartable metrics.
Endpoint:
GET /api/v2/statistics
Supported query parameters:
period
metrics
period is one of:
this_week
last_week
last_7_days
last_30_days
this_month
last_month
this_year
last_year
When omitted, period defaults to last_30_days.
metrics is an optional comma-separated list of stable metric identifiers, for example:
GET /api/v2/statistics?period=last_30_days&metrics=stress,energy,hydration
When omitted, metrics defaults to the authenticated user's enabled Journal metrics. An empty string requests no metrics. Unknown or malformed metric keys and unsupported periods return 400 Bad Request.
The service resolves the selected period using the authenticated user's Nextcloud timezone. All returned dates represent local calendar days and the underlying query range is [from, to). The endpoint never accepts a user ID and all returned data is scoped to the authenticated user.
The response returns canonical values and contains one item for every requested metric, including metrics with no recorded data in the selected period.
{
"period": "last_30_days",
"from": "2026-07-29",
"to": "2026-08-28",
"metrics": [
{
"metricKey": "job_satisfaction",
"category": "daily_value",
"valueType": "scale",
"canonicalUnit": null,
"minimum": 1,
"maximum": 5,
"series": [
{
"date": "2026-07-29",
"value": 2.5,
"subseries": null
},
{
"date": "2026-07-30",
"value": null,
"subseries": null
}
],
"summary": {
"average": 2.8,
"minimum": 1.5,
"maximum": 5,
"count": 26,
"activeDays": 18,
"subseries": null
},
"goals": [
{
"goalId": 42,
"targetKey": "job_satisfaction",
"metricKey": "job_satisfaction",
"kind": "period_value",
"seriesKey": "value",
"comparator": "gte",
"targetValue": 4,
"options": null,
"effectiveFrom": "2026-08-01",
"effectiveTo": null
}
]
},
{
"metricKey": "hydration",
"category": "journal",
"valueType": "event",
"canonicalUnit": null,
"minimum": null,
"maximum": null,
"series": [
{
"date": "2026-07-29",
"value": 6,
"subseries": {
"water": 4,
"coffee": 1,
"tea": 1,
"other": 0
}
}
],
"summary": {
"average": 4.7,
"minimum": 0,
"maximum": 9,
"count": 142,
"activeDays": 24,
"subseries": {
"water": {
"average": 3.3,
"minimum": 0,
"maximum": 6,
"count": 98,
"activeDays": 20,
"subseries": null
}
}
},
"goals": []
}
]
}Each metric object contains the metric identity, its category and value type, the canonical unit when applicable, metric minimum and maximum when defined, daily series, summary, and the goal-revision segments that can be represented as chart thresholds. summary.count is the number of source records, not the number of day buckets; activeDays is the number of day buckets with a recorded numeric point or a non-zero event total. Event metrics and blood pressure provide summaries for their individual series through summary.subseries.
The response also contains sourceRecords: the individual owner-scoped records used to build the selected metric series. Each record has its stable metric key, local date, a numeric value or option value, optional blood-pressure values, and recordedAt for timestamped records. Daily Values have no recordedAt. The records use the same selected-metric and half-open [from, to) filtering as the chart data; they are ordered by local day, then Daily Values before timestamped records, then timestamp and metric key. Notes are deliberately not included.
Every daily point has a subseries field. It is null for ordinary numeric metrics. It is a map for event metrics and blood pressure, where one logical metric has multiple chart series. Hydration uses water, coffee, tea, and other; Break uses its configured event categories; blood pressure uses systolic and diastolic.
Numeric scale and Measurement values use their registry aggregation for each local day. The standard numeric aggregation is arithmetic mean; Kilocalories uses sum across its timestamped kcal Measurements. Allergies is an option Measurement whose daily value is the count of owner-scoped symptom records. A missing numeric or Allergy day is represented by value: null and is excluded from numeric summaries.
Event metrics have a valid daily count of zero. Their series therefore contains zero-valued local days, and their average, minimum, and maximum include every day in the selected period.
Applicable daily goal revisions are returned as separate goals segments with an inclusive effectiveFrom and optional inclusive effectiveTo local date. Clients must not apply a current target retrospectively outside its effective segment. A goal identifies its metricKey, kind, seriesKey, and target options when applicable; seriesKey may be value, an event subseries such as Water or Mindfulness, or Break's total. Incompatible goal types are omitted.
Statistics response definitions are explicit in OpenAPI. Avoid undocumented dynamic response structures.
Saved Statistics views are owner-scoped reusable configurations. They contain a numeric ID, title, native icon, ordered stable metricKeys, a Statistics period, and RFC3339 timestamps. They never contain cached or derived Health values.
GET /api/v2/statistics/views
POST /api/v2/statistics/views
GET /api/v2/statistics/views/{id}
PUT /api/v2/statistics/views/{id}
DELETE /api/v2/statistics/views/{id}
Create and update accept all of title, icon, metricKeys, and period. metricKeys is a non-empty array of stable metric identifiers and period is validated by the same canonical Statistics selection validation as GET /statistics. The authenticated user is the owner; requests never accept a user ID. A missing or another user's ID returns 404 for get, update, and delete.
{
"id": 17,
"title": "Workdays",
"icon": "📈",
"metricKeys": ["stress", "energy"],
"period": "last_30_days",
"createdAt": "2026-09-02T10:00:00Z",
"updatedAt": "2026-09-02T10:00:00Z"
}The MVP does not provide endpoints such as:
/correlations
/insights
/patterns
/recommendations
The statistics API describes recorded data.
It does not interpret relationships between metrics.
A future API may add comparison features without changing the basic Entry model.
Health exposes feature discovery through Nextcloud Capabilities.
Conceptually:
{
"health": {
"apiVersions": [
"2"
],
"features": [
"entries",
"batchEntries",
"goals",
"reminders",
"dailySummaries",
"statistics"
],
"modules": [
"stress",
"energy",
"mood",
"hydration",
"break",
"movement",
"sleep",
"weight"
]
}
}External clients should use capabilities to detect available functionality where appropriate.
All API input is validated server-side.
Examples of invalid requests include:
stress = 99
hydration = "bucket"
sleep_duration = -120
unknown metric key
invalid context
invalid timestamp
The Vue frontend may provide client-side validation for UX purposes, but it is not authoritative.
OCS endpoints must use official OCS exception types.
Typical semantics include:
- bad request for malformed or invalid input
- not found for unavailable owned resources
- forbidden where a request is authenticated but explicitly not permitted
Do not leak:
- database errors
- SQL
- stack traces
- internal class names
- another user's data existence
Error messages should be safe and useful to API consumers.
For an endpoint such as:
GET /entries/4711
the implementation must query using both:
authenticated user
+
entry ID
Do not:
- retrieve the entry globally by ID
- return it
- rely on the frontend not to request other IDs
The same rule applies to:
- goals
- reminders
- user configuration
- statistics
- summaries
Every endpoint requires API tests.
Tests are the primary automated testing priority for the MVP.
Minimum coverage includes:
Unauthenticated requests fail.
Valid requests return the documented response.
Invalid values are rejected.
User A cannot access User B's resources.
Create, update and delete operations behave as documented.
Cursor pagination:
- does not duplicate entries
- does not skip entries under stable data
- preserves deterministic ordering
- respects the maximum limit
The resulting implementation remains extractable by the official OpenAPI tooling.
Integration/API tests should use at least two distinct users.
Conceptually:
health-user-a
health-user-b
Cross-user tests must be explicit.
Example:
- User A creates Entry 4711.
- User B requests Entry 4711.
- User B must not receive User A's data.
- User B must not update Entry 4711.
- User B must not delete Entry 4711.
Reusable public response structures should be centrally defined in:
lib/ResponseDefinitions.php
where appropriate.
Examples may include:
Entry
Module
MetricDefinition
UserModuleConfiguration
Goal
Reminder
DailySummary
NumericStatistics
EventStatistics
PaginatedEntries
Do not duplicate incompatible response shapes across controllers.
Every endpoint must document:
- purpose
- parameters
- request fields
- response structure
- HTTP status codes
- possible documented errors
Every API parameter must have an explicit type.
Every response must have an explicit type.
Public API methods without sufficient information for OpenAPI extraction are incomplete.
Health API v2 should evolve additively where possible.
Safe examples:
- adding a new endpoint
- adding a new built-in metric
- adding an optional response field
- adding a new capability
Potentially breaking examples:
- renaming
metricKey - changing the meaning of an existing metric
- changing numeric units
- removing response fields
- changing stable enum values
- changing endpoint paths
Breaking changes require careful review and may require Health API v3.
For every new feature, implement in this order:
1. Define API contract
2. Define response types
3. Implement domain/service logic
4. Implement persistence
5. Implement OCS controller
6. Add API tests
7. Verify OpenAPI extraction
8. Implement frontend API client
9. Implement Vue UI
Do not begin with the frontend and invent the API afterward.
Initial Health API v2:
GET /api/v2/modules
GET /api/v2/configuration
PUT /api/v2/configuration
GET /api/v2/entries
POST /api/v2/entries
POST /api/v2/entries/batch
GET /api/v2/entries/{id}
PUT /api/v2/entries/{id}
DELETE /api/v2/entries/{id}
GET /api/v2/goals
POST /api/v2/goals
PUT /api/v2/goals/{id}
DELETE /api/v2/goals/{id}
GET /api/v2/reminders
POST /api/v2/reminders
PUT /api/v2/reminders/{id}
DELETE /api/v2/reminders/{id}
GET /api/v2/summaries/days
GET /api/v2/statistics
GET /api/v2/statistics/views
POST /api/v2/statistics/views
GET /api/v2/statistics/views/{id}
PUT /api/v2/statistics/views/{id}
DELETE /api/v2/statistics/views/{id}
GET /api/v2/daily-notes/{date}
PUT /api/v2/daily-notes/{date}
This is the planned MVP API surface.
Do not add additional public endpoints unless a concrete requirement cannot be expressed through this model.
date is a strict local YYYY-MM-DD key. GET returns { date, content, createdAt, updatedAt }; when no note exists, content, createdAt, and updatedAt are explicitly null. PUT accepts { content }, where content is plain text of at most 2000 characters, and returns the same representation. Nextcloud rich-content UI controls may be used only when they preserve this plain-text contract; HTML is never stored.
The Configuration response profile contains canonical heightCm, the display preference heightDisplayUnit, optional dateOfBirth as strict YYYY-MM-DD, and optional growthReferenceSex (female or male). Profile updates remain owner-scoped and use the existing configuration endpoint. Height is stored canonically in centimetres; date of birth and reference sex are used only for a future verified BMI-for-age calculation and must not infer medical information.
searchDailyNotes is an owner-scoped integration preference in the Configuration response. It defaults to false, including for existing users after an upgrade. When enabled, the native Nextcloud Unified Search provider searches only the requesting user's Daily Notes; it never exposes notes to another user. Search does not add a Health data endpoint or change the Daily Note plain-text API contract.
GET /goals returns owner-scoped active and paused logical goals plus the public, non-personal target registry; retired identities are excluded from goal management. POST /goals creates an identity with targetKey, period, comparator (gte or lte), canonical targetValue, and remindersEnabled. Identity uniqueness is (user, targetKey, period), so distinct daily, weekly, monthly, or long-term goals may coexist where supported while an identical target/period duplicate is rejected. PUT /goals/{id} updates the owner-scoped goal; active: false pauses without retiring it and active: true resumes that same identity. Value/direction changes create or update the current local-period revision, while target/period changes retire the old identity and create a new one. DELETE /goals/{id} retires rather than deletes history. GET /goals/progress?period=day|week|month|long_term&date=YYYY-MM-DD returns derived owner-scoped progress; finite dates are interpreted in the user's Nextcloud timezone and future periods are rejected. Long-term latest-value responses include baselineValue and a clamped directional progressRatio. Job Satisfaction uses the existing daily-value endpoint with metricKey: "job_satisfaction", integer numeric values 1–5, and unit: null.
Kilocalories uses POST /measurements: each non-negative numeric record has fixed unit kcal and an RFC3339 recordedAt; multiple owner-scoped records may be created for one local day. Day-based goal progress and Statistics use their sum. PUT /daily-values/kilocalories/{date} is intentionally rejected because Kilocalories is no longer a Daily Value. Existing v3 Daily Value calorie rows are converted by the forward migration to noon-local Measurements and are retained as source rows. Fruit continues to use PUT /daily-values/fruit/{date} and accepts a non-negative whole number with fixed unit pieces. Unsupported units, negative values, and fractional Fruit values return 400.
Allergies uses the generic POST /measurements and PUT /measurements/{id} endpoints with metricKey: "allergies", numericValue: null, values: null, unit: null, and one canonical optionValue. Supported values are sneezing, runny_nose, nasal_congestion, itchy_nose, itchy_eyes, watery_eyes, red_eyes, swollen_eyelids, cough, scratchy_throat, wheezing, shortness_of_breath, itchy_skin, and hives. A note is optional. Labels and their nasal, eye, respiratory, and skin presentation groups are localized client concerns and are never persisted. Unknown option values return 400; all records are owner-scoped. Daily Journal and Statistics values count entries without deriving severity or medical meaning.
POST /entries and POST /measurements accept an optional UUID v4 operationId. Repeating a request for the same authenticated owner and metric with the same operation ID returns the existing logical record instead of creating a duplicate. Reusing it for another metric is rejected. The field supports offline replay; it is not an owner selector and is not included in response resources. Daily values remain idempotent by their existing owner/metric/local-date PUT identity.