Tags
newLabels for events, states, and context
Introduction
Biomarkers and scores capture what the body does. Tags capture everything else—a symptom, a medication, a shift, a subscription—as discrete, time-bound records ready to query, stream, and correlate with health signals.
Tags can come from anywhere—the Sahha SDK reading device health platforms, Sahha's platform integrations with Garmin, Oura, and other connected services, or direct API calls from your own app or backend. Whatever the source, every tag arrives in the same shape: a name, an optional value, a type, and a time range.
Related products
Need numeric metrics like steps, sleep duration, or heart rate? See Biomarkers . Need raw sensor samples? See Data Logs . Need long-term behavioural labels? See Archetypes .
Key Features
Auto-Collected
Reserved tags arrive automatically from the Sahha SDK (reading HealthKit and Health Connect) and from server-to-server integrations with Garmin, Oura, and other connected platforms—all normalized to the same names so your code never branches on source
Events and States
One schema models both moments in time (a dose, a test result) and windows of time (a shift, a subscription)—including open-ended states that stay active until you close them
Analytics-Ready
A strict category > name > value taxonomy with value as a top-level indexed field, so dose-response and severity queries work without parsing free-text notes
Idempotent
Identity is derived from name, type, source, and start time—re-upload the same tuple to update or close a tag, with no risk of duplicates from retries or follow-ups
Custom + Reserved
Send any custom category, name, and value alongside Sahha's reserved namespace—one schema, one pipeline, one set of webhooks for both
Real-time Streaming
Every tag streams via webhooks the moment it lands—useful for triggering nudges, segmentation, or downstream analytics in real time
How It Works
A tag is a structured record with a name (what's being tracked), an optional value (the variant, severity, or dose), a type ( event or state), and a time range. The same schema is used whether the tag is a clinical signal collected by the SDK or a business event posted from your backend.
Events are points in time—a moment was recorded and nothing more. endDateTime must be null.
States span windows. A closed state has both startDateTime and endDateTime set. An open state has endDateTime: null, meaning the window is still active; close it later by re-uploading the same tag with an endDateTime set.
Identity is the tuple, not the id. Each tag's identity is derived from profile + name + type + source + startDateTime. The id field is server-generated; any value supplied on upload is ignored. Re-uploading a tag with the same identity tuple updates the existing record, so retries after transient errors and follow-up edits never create duplicates.
Tags flow into Sahha through three channels: the mobile SDK, Sahha's platform integrations with services like Garmin and Oura, and direct API calls from your backend or app. Reserved names must follow the schema below regardless of channel; custom names can use any shape. Reserved tags with a non-matching shape are rejected per-item (200 OK with the rejection reported in warnings)—the rest of the batch still lands. Tags flow out via the API and webhooks in the same shape they came in.
List of Tags (Reserved)
Sahha maintains a curated namespace of tag names with a fixed schema . Reserved tags can come from any source—the Sahha SDK (reading HealthKit and Health Connect), Sahha's platform integrations with Garmin, Oura, and other connected services, or your own app or backend—as long as the tag's type, category, and value match the entry below. Names inside this namespace arrive in a consistent shape across platforms and may power native analytics in future releases.
The namespace will grow over time. Currently it covers two categories: symptom and reproductive.
Symptom
Every symptom tag is logged with the same value set—a five-level severity scale—and arrives as a point-in-time event.
| Name | Type | Possible Values |
|---|---|---|
| abdominal_cramps | event | unknown, not_present, mild, moderate, severe |
| acne | event | unknown, not_present, mild, moderate, severe |
| appetite_changes | event | unknown, not_present, mild, moderate, severe |
| bladder_incontinence | event | unknown, not_present, mild, moderate, severe |
| bloating | event | unknown, not_present, mild, moderate, severe |
| breast_pain | event | unknown, not_present, mild, moderate, severe |
| chills | event | unknown, not_present, mild, moderate, severe |
| constipation | event | unknown, not_present, mild, moderate, severe |
| coughing | event | unknown, not_present, mild, moderate, severe |
| diarrhea | event | unknown, not_present, mild, moderate, severe |
| dizziness | event | unknown, not_present, mild, moderate, severe |
| dry_skin | event | unknown, not_present, mild, moderate, severe |
| fainting | event | unknown, not_present, mild, moderate, severe |
| fatigue | event | unknown, not_present, mild, moderate, severe |
| fever | event | unknown, not_present, mild, moderate, severe |
| generalized_body_ache | event | unknown, not_present, mild, moderate, severe |
| hair_loss | event | unknown, not_present, mild, moderate, severe |
| headache | event | unknown, not_present, mild, moderate, severe |
| heartburn | event | unknown, not_present, mild, moderate, severe |
| hot_flashes | event | unknown, not_present, mild, moderate, severe |
| loss_of_smell | event | unknown, not_present, mild, moderate, severe |
| loss_of_taste | event | unknown, not_present, mild, moderate, severe |
| lower_back_pain | event | unknown, not_present, mild, moderate, severe |
| memory_lapse | event | unknown, not_present, mild, moderate, severe |
| mood_changes | event | unknown, not_present, mild, moderate, severe |
| nausea | event | unknown, not_present, mild, moderate, severe |
| night_sweats | event | unknown, not_present, mild, moderate, severe |
| pelvic_pain | event | unknown, not_present, mild, moderate, severe |
| rapid_pounding_or_fluttering_heartbeat | event | unknown, not_present, mild, moderate, severe |
| runny_nose | event | unknown, not_present, mild, moderate, severe |
| shortness_of_breath | event | unknown, not_present, mild, moderate, severe |
| sinus_congestion | event | unknown, not_present, mild, moderate, severe |
| skipped_heartbeat | event | unknown, not_present, mild, moderate, severe |
| sleep_changes | event | unknown, not_present, mild, moderate, severe |
| sore_throat | event | unknown, not_present, mild, moderate, severe |
| vaginal_dryness | event | unknown, not_present, mild, moderate, severe |
| vomiting | event | unknown, not_present, mild, moderate, severe |
| wheezing | event | unknown, not_present, mild, moderate, severe |
Reproductive
Reproductive tags use a mix of event and state types, each with its own closed value set (or no value for presence-only flags).
| Name | Type | Possible Values |
|---|---|---|
| menstrual_flow | event | unknown, not_present, light, medium, heavy |
| intermenstrual_bleeding | event | — |
| infrequent_menstrual_cycles | event | — |
| irregular_menstrual_cycles | event | — |
| persistent_intermenstrual_bleeding | event | — |
| prolonged_menstrual_periods | event | — |
| pregnancy | state | — |
| lactation | state | — |
| ovulation_test | event | inconclusive, negative, high, positive |
| cervical_mucus | event | unknown, dry, sticky, creamy, watery, egg_white, unusual |
| sexual_activity | event | unknown, protected, unprotected |
| contraceptive | state | unknown, implant, injection, intravaginal_ring, iud, oral, patch |
| pregnancy_test | event | inconclusive, negative, positive |
| progesterone_test | event | inconclusive, negative, positive |
Reserved Additional Properties
The additionalProperties field is free-form on every tag—use any keys you need on custom tags. The keys below are populated by the SDK on specific reserved tags, so custom code shouldn't redefine them on the tags they appear on.
| Key | Emitted on | Values |
|---|---|---|
cycle_start | menstrual_flow | "true" or "false" |
Custom Tags
Outside the reserved namespace there is no fixed schema —pick any category, name, and value your product needs. Custom tags use the same record shape, the same API, and the same webhook stream as reserved ones; they're stored and returned exactly as sent.
Typical custom use cases:
- Workforce & shifts —
night_shift,on_call,field_deployment - Subscription & lifecycle —
premium_tier,trial,lapsed,onboarding_complete - Clinical & coaching —
medication,therapy_session,intervention_arm - Habits & lifestyle —
caffeine,alcohol,meal,meditation
Built-in analytics interpret reserved names only. Custom tags are first-class for storage and delivery, and any reporting you build on top can mix both freely.
Use Cases
Symptom & Wellness Journaling
Let users log how they feel against the body's signals—correlate flares with sleep, activity, or stress
Clinical & Reproductive Health
Capture medications, treatments, and reproductive events with timestamped accuracy, ready for study analysis
Workforce & Operations
Tag shifts, on-call windows, or deployment periods to see how operating conditions affect recovery and readiness
Subscription & Cohort Analysis
Track plan tiers, trial status, or program enrolment as ongoing states for segmentation and retention insights
Feature Impact
Tag exposure to product features as states and measure their effect on health outcomes over time
Coaching & Intervention
Trigger nudges based on what the user is doing right now—not just what the body is doing
Output Schema
Every tag returns a consistent JSON structure regardless of source or category.
id UUID Server-generated. Ignored on upload—identity is derived from name, type, source, and startDateTime under the profile
type string event (point in time) or state (window of time)
category string nullableOptional bucket—reserved (symptom, reproductive) or any custom value
name string The primary signal—reserved name or custom
value string nullableOptional severity, dose, or variant. May be null for presence-only flags
source string Which system produced the tag. SDK and integrations set this automatically; backend uploads typically use a reverse-DNS string (e.g. acme.workforce)
startDateTime datetime When the tag began (ISO 8601)
endDateTime datetime nullableWhen the tag ended. Must be null for events; may be null for open states
receivedAtUtc datetime UTC timestamp when Sahha received the tag
additionalProperties object<string,string> nullableAuxiliary metadata. Sahha uses reserved keys only; custom keys are stored and returned verbatim
{ "id": "f3b1a8e0-4c5d-4e6f-9a8b-1c2d3e4f5a01", "type": "state", "category": "work", "name": "night_shift", "value": "12_hour", "source": "acme.workforce", "startDateTime": "2024-02-09T19:00:00+00:00", "endDateTime": "2024-02-10T07:00:00+00:00", "receivedAtUtc": "2024-02-10T07:05:02+00:00", "additionalProperties": { "role": "icu_nurse", "overtime": "false" }} {
"id": "f3b1a8e0-4c5d-4e6f-9a8b-1c2d3e4f5a01",
"type": "state",
"category": "work",
"name": "night_shift",
"value": "12_hour",
"source": "acme.workforce",
"startDateTime": "2024-02-09T19:00:00+00:00",
"endDateTime": "2024-02-10T07:00:00+00:00",
"receivedAtUtc": "2024-02-10T07:05:02+00:00",
"additionalProperties": {
"role": "icu_nurse",
"overtime": "false"
}
}
More examples
An open state —still active, no end yet. Close it later by posting a follow-up tag with the same name and an endDateTime.
{ "id": "f3b1a8e0-4c5d-4e6f-9a8b-1c2d3e4f5a02", "type": "state", "category": "subscription", "name": "premium_tier", "value": "annual", "source": "acme.billing", "startDateTime": "2024-01-28T00:00:00+00:00", "endDateTime": null, "receivedAtUtc": "2024-01-28T00:00:16+00:00", "additionalProperties": { "autoRenew": "true", "priceUsd": "99" }} {
"id": "f3b1a8e0-4c5d-4e6f-9a8b-1c2d3e4f5a02",
"type": "state",
"category": "subscription",
"name": "premium_tier",
"value": "annual",
"source": "acme.billing",
"startDateTime": "2024-01-28T00:00:00+00:00",
"endDateTime": null,
"receivedAtUtc": "2024-01-28T00:00:16+00:00",
"additionalProperties": {
"autoRenew": "true",
"priceUsd": "99"
}
}
An event with a dose — type: "event" requires endDateTime: null. The value field carries the dose at the top level so analytics can aggregate without parsing notes.
{ "id": "f3b1a8e0-4c5d-4e6f-9a8b-1c2d3e4f5a03", "type": "event", "category": "medication", "name": "insulin_bolus", "value": "6_units", "source": "acme.clinical", "startDateTime": "2024-02-11T12:30:00+00:00", "endDateTime": null, "receivedAtUtc": "2024-02-11T12:30:10+00:00", "additionalProperties": { "timing": "before_breakfast" }} {
"id": "f3b1a8e0-4c5d-4e6f-9a8b-1c2d3e4f5a03",
"type": "event",
"category": "medication",
"name": "insulin_bolus",
"value": "6_units",
"source": "acme.clinical",
"startDateTime": "2024-02-11T12:30:00+00:00",
"endDateTime": null,
"receivedAtUtc": "2024-02-11T12:30:10+00:00",
"additionalProperties": {
"timing": "before_breakfast"
}
}
FAQ
Biomarkers are for numeric metrics (steps, heart rate, sleep duration). Tags are for categorical signals—anything described by a name, a variant, or a severity rather than a number. Symptom severity, a positive test result, an active subscription, and a 12-hour shift are all tags.
Yes. Reserved names aren't off-limits to your code—they just have a fixed schema. As long as the tag's type, category, and value match the entry for that name in the reserved list , you can write it from anywhere: the SDK, your app, or your backend. Posts with a different shape are rejected for that tag only and may be excluded from native analytics.
Only that tag is rejected—other tags in the same batch still land. The upload returns 200 OK with null at the rejected slot in the positional ids array and an entry in warnings explaining why (for example, a value outside the reserved enum). Custom (non-reserved) tags are never rejected by this validator.
Yes. Outside the reserved namespace there's no fixed schema—use any category, name, and value you need. Custom tags are stored and returned exactly as sent and stream via webhooks alongside reserved tags. Sahha's built-in analytics interpret reserved names only, but any reporting you build is free to use both.
Re-upload the same tag with endDateTime set. As long as name, type, source, and startDateTime match the original, Sahha treats it as the same record and updates it in place. To keep the open and closed records as separate timeline entries instead, change one of those identity fields (typically startDateTime) so they form a distinct tuple.
Tags flow in from three places: the Sahha SDK reading device health platforms, Sahha's platform integrations with services like Garmin and Oura, or direct API calls from your own app or backend. Reserved or custom, every tag uses the same shape and pipeline. Use the source field on each tag to record where it came from.
Sahha derives each tag's identity from profile + name + type + source + startDateTime, not from the id field (which is server-generated and ignored on upload). Re-uploading the same identity tuple updates the existing record, so retries after transient errors and follow-up edits never create duplicates.
Getting Started
Write and read tags via REST for any profile
Reserved tags are collected automatically; post custom tags from your app
Stream every tag in real time as it lands
Support
For help with Tags, reach out in the Slack community or contact support@sahha.ai .
- Previous
- Biomarkers
- Next
- Data Logs