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.
Three steps. The whole data plane is plain HTTPS + JSON.
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"}'
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"
}]
}'
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"
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.
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: BearerEnd 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.
| Source | How it ingests | Gives you |
|---|---|---|
| device | You POST it | Apple Health / Android Health Connect — 25 sample types |
| oura | Webhook + sync | Sleep, HR, HRV, activity, readiness, temperature, SpO2 |
| whoop | Webhook + backfill | Sleep, HR, HRV, workouts, recovery |
| withings | Notify + sync | Weight, body composition, blood pressure, SpO2, temperature |
| garmin | Push only | Daily activity, sleep, workouts, resting HR, stress |
| fitbit | Subscription + sync | Sleep, workouts, weight, daily activity |
| strava | Webhook + sync | Workouts |
| dexcom | Pull only | Continuous glucose |
| google_calendar outlook_calendar icloud_calendar | Cursor pull | Events — times, busy state, attendee count |
| gmail outlook_mail | Cursor pull | Metadata only — never bodies |
| EHR | SMART on FHIR | Hospital records and Medicare claims (separate plane) |
| Method | Path | Auth |
|---|---|---|
| POST | /ingest/device | API key |
| GET | /me/records | API key + user |
| GET | /me/export | API key + user |
| GET | /me/profile | API key + user |
| GET | /me/sources | API key + user |
| POST | /me/sources/:source/connect | API key + user |
| POST | /me/sources/:source/sync | API key + user |
| DELETE | /me/data | API key + user |
| POST | /me/ehr/consent | API key + user |
| POST | /me/ehr/connect | API key + user |
| GET | /me/ehr/records | API key + user |
| GET | /me/ehr/access-log | API key + user |
| POST | /auth/register | none |
| POST | /developer/api-keys | JWT |
Full request and response schemas are in the interactive OpenAPI reference.