Health Connect · Build

Ship a health integration in one prompt

Create an API key, copy the agent prompt below into Claude Code, Cursor or any coding agent, and it has everything it needs — every endpoint, every record type, every unit, and the traps that cost other teams a day. No SDK to install.

Quickstart

Three steps. The whole data plane is plain HTTPS + JSON.

  1. Create an API key

    Register at the dashboard and click Create key. The plaintext is shown once. Or over HTTP:

    curl -X POST https://connect.neemhealth.ai/auth/register \
      -H 'content-type: application/json' \
      -d '{"email":"you@example.com","password":"a-strong-password","name":"Acme"}'
    
    curl -X POST https://connect.neemhealth.ai/developer/api-keys \
      -H "authorization: Bearer <accessToken>" \
      -H 'content-type: application/json' \
      -d '{"name":"production-server"}'
  2. Send data in

    Your app chooses the userId — any opaque string. It is namespaced to your tenant, so it can never collide with another developer's.

    curl -X POST https://connect.neemhealth.ai/ingest/device \
      -H "X-API-Key: hcp_live_..." \
      -H 'content-type: application/json' \
      -d '{
        "userId": "end-user-123",
        "records": [{
          "sampleType": "heart_rate",
          "sourceUuid": "HK-UUID-1",
          "start": "2026-09-01T08:00:00Z",
          "end":   "2026-09-01T08:00:00Z",
          "value": 62,
          "unit":  "bpm"
        }]
      }'
  3. Read it back

    Same shape whichever source it came from.

    curl "https://connect.neemhealth.ai/me/records?type=heart_rate_sample&limit=50" \
      -H "X-API-Key: hcp_live_..." \
      -H "x-user-id: end-user-123"

The agent prompt

Copy this into your coding agent. It contains the full API surface, the canonical model, and the failure modes that are worth knowing before you hit them. The agent will ask what you are building and implement it.

Health Connect — full integration brief
You are integrating an application with the Health Connect API.

Ask me what I am building and which data I need before writing code. Then
implement it end to end. Everything you need is below — do not guess at
endpoints or invent an SDK, there isn't one. It is plain HTTPS and JSON.

================================================================
1. WHAT THIS PLATFORM IS
================================================================
Health Connect ingests health data from many sources, normalises every one of
them into a single canonical model, and serves it over one API. Add a source
and the read code does not change.

Base URL: https://connect.neemhealth.ai
Interactive reference: https://connect.neemhealth.ai/docs

There are two auth surfaces. Do not mix them.

  Data plane   — ingest and read.  Header: X-API-Key: hcp_live_...
  Control plane— account, API keys. Header: Authorization: Bearer 

End users are opaque strings YOU choose (userId). They are namespaced to your
tenant, so "user-1" in your app is unrelated to "user-1" in anyone else's.
Nearly every data-plane call needs BOTH the API key and a user:

  X-API-Key: hcp_live_...
  x-user-id: end-user-123

================================================================
2. GETTING A KEY
================================================================
POST /auth/register     {email, password (min 8), name}  -> {accessToken, refreshToken}
POST /auth/login        {email, password}                -> same
POST /auth/refresh      {refreshToken}                   -> new pair (old one is revoked)
POST /developer/api-keys {name}   [Bearer JWT]  -> {plaintext}  ** SHOWN ONCE **
GET  /developer/api-keys          [Bearer JWT]  -> list, never the secret
DELETE /developer/api-keys/:id    [Bearer JWT]  -> 204

The access token lasts ~15 minutes. Refresh tokens rotate on use.
Store the API key server-side. It is TENANT-WIDE: anything that can read it can
read every one of your users' data. Never ship it in a mobile binary or browser
bundle — proxy through your own backend.

================================================================
3. SENDING DATA IN
================================================================
POST /ingest/device
  {"userId": "...", "records": [ ...samples... ]}

Only `device` is client-pushed. Every cloud source (Oura, Withings, Dexcom,
calendars, EHR) is ingested server-side after the user connects it — you never
POST those.

One device sample:
  {
    "sampleType": "heart_rate",     // see the list below
    "sourceUuid": "HK-UUID-1",      // stable id from HealthKit/Health Connect
    "start": "2026-09-01T08:00:00Z",// ISO-8601 WITH offset
    "end":   "2026-09-01T08:00:00Z",
    "value": 62,                    // required for quantity samples
    "unit":  "bpm"                  // optional, but CHECKED if present
  }

Ingest is idempotent. sourceUuid is the dedup key — re-sending the same sample
updates in place and never duplicates. Retry freely.

sampleType values (25):
  sleep, hrv, heart_rate, resting_heart_rate, walking_heart_rate, steps,
  active_energy, distance, workout, mindful_session, basal_body_temperature,
  wrist_temperature, body_temperature, blood_oxygen, respiratory_rate,
  vo2_max, menstrual_flow, ovulation_test, blood_pressure, weight,
  body_fat_percentage, lean_body_mass, dietary_energy, blood_glucose,
  state_of_mind

Extra fields, per sampleType:
  blood_pressure  -> systolic + diastolic REQUIRED (mmHg, one sample not two),
                     optional posture, optional value = cuff pulse
  workout / mindful_session -> activityType; optional value = kcal
  menstrual_flow / ovulation_test -> code
  dietary_energy  -> optional mealType
  blood_glucose   -> optional origin ("cgm"|"fingerstick"), optional mealContext
  state_of_mind   -> value is a valence from -1 to 1; optional kind, labels
  sleep           -> optional stages {awake, light, deep, rem} in SECONDS
  steps/active_energy/distance -> set aggregate:true if it is a whole-day total

UNITS ARE NOT CONVERTED. A value is stored exactly as sent. If `unit` is
present it is validated and a mismatch is a 422 — that is deliberate, because a
silently mis-scaled health value is worse than a rejected request.
  HRV ms · energy kcal · distance m · temperature degC · heart rate bpm
  SpO2 percent 0-100 · respiratory rate count/min · VO2max ml/kg/min
  blood pressure mmHg · mass kg · body fat percent 0-100 · glucose mg/dL

Four rejections that are easy to hit:
  * blood_oxygen or body_fat_percentage sent as a 0-1 fraction. HealthKit
    reports both that way. 0.24 body fat is a believable percentage AND a
    believable fraction, so nothing downstream could ever catch it. Multiply
    by 100.
  * blood_pressure with diastolic >= systolic. That is a transposition, not a
    low reading.
  * state_of_mind outside -1..1. Normalise your source's own scale first.
  * blood_glucose in mmol/L. Convert to mg/dL (multiply by 18.0182).

An UNKNOWN sampleType is skipped and logged, not rejected — enabling a new
HealthKit type client-side will never break the rest of your batch. A KNOWN
type with a malformed body still 422s, because that is your bug.

================================================================
4. CONNECTING CLOUD SOURCES
================================================================
POST /me/sources/:source/connect   {redirectUri, returnUrl?}
     -> {authorizationUrl}   send the user there
GET  /me/sources                   -> per-source state + lastSyncAt + lastError
POST /me/sources/:source/sync      -> force a resync now
POST /me/sources/:source/credentials  (non-OAuth, e.g. iCloud app password)

:source is one of
  whoop, oura, withings, strava, garmin, fitbit, dexcom,
  google_calendar, outlook_calendar, icloud_calendar, gmail, outlook_mail

Pass returnUrl and the callback 302s back to it with ?source=...&status=...,
which is what you want for a mobile deep link.

A source with no credentials configured on the server returns 404 from
/connect. That is graceful degradation, not an outage — check /me/sources or
https://connect.neemhealth.ai/status.json to see what is live right now.

================================================================
5. READING DATA
================================================================
GET /me/records?type=&source=&from=&to=&limit=&before=
  type    one of the 17 record types, lowercase
  source  device | whoop | oura | withings | strava | garmin | fitbit |
          dexcom | google_calendar | outlook_calendar | icloud_calendar |
          gmail | outlook_mail
  from/to ISO-8601, inclusive, on recordedAt
  limit   1-500, default 100
  before  pagination cursor — pass the previous page's nextBefore
  -> {records: [{type, source, sourceId, recordedAt, payload}], nextBefore}

GET /me/profile   every connected integration + per-source record counts,
                  enough to render a profile screen in ONE call
GET /me/export    every record, NDJSON, oldest first, streamed.
                  Use this for data-portability, not /me/records in a loop:
                  the paged read may skip records sharing a millisecond across
                  a page boundary, the export cannot.
DELETE /me/data   erase everything for one user. Irreversible.

The 17 record types:
  sleep_session, heart_rate_sample, hrv_sample, daily_activity, workout,
  calendar_event, email_message, temperature_sample, blood_oxygen_sample,
  respiratory_rate_sample, wellness_score, user_annotation,
  blood_pressure_sample, body_composition, glucose_sample, nutrition_entry,
  mood_entry

Payload rules that will bite you if you ignore them:
  * `aggregate: true` marks a source-computed total or average. Oura and
    Garmin send one summed DAILY_ACTIVITY per day; a phone sends hundreds of
    increments. Summing both double-counts. Filter on this flag.
  * Resting heart rate arrives as HEART_RATE_SAMPLE with context:"rest" and
    aggregate:true. Exclude aggregates before averaging a raw stream.
  * TEMPERATURE_SAMPLE carries EITHER celsius (absolute) OR deviationCelsius
    (from that wearer's own baseline). Never average the two. Which one is
    present depends on the source; measurementType answers a different
    question (what was measured), not which field is set.
  * GLUCOSE_SAMPLE has origin "cgm" or "fingerstick". Do not average them —
    a CGM reads interstitial fluid with a lag.
  * MOOD_ENTRY kind is "momentary" or "daily". Do not sum them.
  * Calendar events store attendeeCount, never attendee identities. Titles
    only if the operator opted in. Email stores the sender DOMAIN only, never
    the address, and never message bodies.

================================================================
6. CLINICAL RECORDS (separate plane)
================================================================
Medical records live in a physically separate database, are consent-gated and
audited on every access. They are NOT returned by /me/records.

POST   /me/ehr/consent   {scope:"EHR_FETCH"}   grant first, or everything 403s
GET    /me/ehr/consent
DELETE /me/ehr/consent
GET    /me/ehr/organizations           search connectable health systems
POST   /me/ehr/connect  {vendor, issuer} -> {authorizationUrl}  (SMART, PKCE)
GET    /me/ehr/connections
POST   /me/ehr/connections/sync
GET    /me/ehr/records?issuer=...      one hospital's chart
GET    /me/ehr/access-log              who read this patient's data, and why
DELETE /me/ehr/connections

Records are plain FHIR R4 resources, keyed by issuer so two hospitals never
collide. CMS Blue Button (vendor CMS_BLUE_BUTTON) serves Medicare CLAIMS —
Coverage and ExplanationOfBenefit — through the same flow.

The whole surface returns 503 when the deployment has EHR turned off. Handle
that as "not available here", not as an error.

================================================================
7. ERRORS
================================================================
  400  malformed request (a missing required field)
  401  missing or invalid API key / JWT
  403  consent required (clinical plane only)
  404  unknown source, or a source with no credentials configured
  422  the payload failed validation — the message names the field. This is
       the one to surface in logs; it is always your side.
  429  rate limited. Limits are PER TENANT, not per IP, so your own backend
       calling on behalf of many users shares one bucket. Back off.
  503  a feature is disabled in this deployment

================================================================
8. HOW TO BUILD IT
================================================================
  * Keep the API key server-side. Always.
  * Retries are safe everywhere — ingest is idempotent on (source, sourceId).
  * Read /me/profile once to render a screen; do not fan out per source.
  * Page with `before`, not an offset.
  * Treat every payload field as optional except those marked required.
    Sources genuinely differ in what they report; check presence rather than
    assuming, and never render 0 for absent.
  * Store timestamps as received. They are ISO-8601 and already correct.
  * Poll /status.json if you want to show integration health in your own UI.

Now ask me what I am building.
The prompt is the whole contract. It is written so an agent needs nothing else — no SDK, no docs tab, no second prompt. If your agent asks a question it answers, tell us at admin@neemhealth.ai and the prompt gets fixed.

Reference

Sources

SourceHow it ingestsGives you
deviceYou POST itApple Health / Android Health Connect — 25 sample types
ouraWebhook + syncSleep, HR, HRV, activity, readiness, temperature, SpO2
whoopWebhook + backfillSleep, HR, HRV, workouts, recovery
withingsNotify + syncWeight, body composition, blood pressure, SpO2, temperature
garminPush onlyDaily activity, sleep, workouts, resting HR, stress
fitbitSubscription + syncSleep, workouts, weight, daily activity
stravaWebhook + syncWorkouts
dexcomPull onlyContinuous glucose
google_calendar
outlook_calendar
icloud_calendar
Cursor pullEvents — times, busy state, attendee count
gmail
outlook_mail
Cursor pullMetadata only — never bodies
EHRSMART on FHIRHospital records and Medicare claims (separate plane)

Endpoints at a glance

MethodPathAuth
POST/ingest/deviceAPI key
GET/me/recordsAPI key + user
GET/me/exportAPI key + user
GET/me/profileAPI key + user
GET/me/sourcesAPI key + user
POST/me/sources/:source/connectAPI key + user
POST/me/sources/:source/syncAPI key + user
DELETE/me/dataAPI key + user
POST/me/ehr/consentAPI key + user
POST/me/ehr/connectAPI key + user
GET/me/ehr/recordsAPI key + user
GET/me/ehr/access-logAPI key + user
POST/auth/registernone
POST/developer/api-keysJWT

Full request and response schemas are in the interactive OpenAPI reference.