API reference
Mobile ingest: Apple Health and Health Connect
Apple Health (HealthKit) and Health Connect have no cloud API: the data lives on the phone. Your app reads it with the user's permission and uploads it to Latido, which stores it next to every other provider's data — same model, same read endpoints, same webhooks, same MCP tools.
Status. Latido's native iOS / Android SDKs are not available yet. What exists today is the upload endpoint below, which your own app calls (and the native SDKs will call when they ship).
The endpoint
POST /v1/members/{memberId}/ingest — latido.ingest.upload(memberId, input)
- Live API keys only. A test key gets
403 FORBIDDEN("Test keys can only use the demo provider"). - Call it from your backend, not from the phone: the request carries a project API key. The phone sends what it read to your server, which forwards it.
- The member must exist (create it with
POST /v1/membersor a connect session).
{
"provider": "apple_health",
"records": [
{
"type": "workout",
"providerRecordId": "9C1D3E8A-0F4B-4C5D-8E6F-1A2B3C4D5E6F",
"sport": "running",
"providerSport": "HKWorkoutActivityTypeRunning",
"startTime": "2026-10-20T07:00:00+02:00",
"endTime": "2026-10-20T07:48:00+02:00",
"utcOffsetMinutes": 120,
"durationSeconds": 2880,
"distanceMeters": 8200,
"activeKilocalories": 610,
"averageHeartRateBpm": 151,
"hasFile": false,
"source": { "appName": "com.apple.health", "deviceName": "Apple Watch" }
}
]
}| Field | Notes |
|---|---|
provider |
apple_health or health_connect: the on-device store the records were read from. |
records |
1-1000 records. Each is a normalized record of the data model without the fields Latido assigns: no id, memberId, connectionId, provider, createdAt, updatedAt. The type field selects the family; both stores accept all six (workout, daily_summary, sleep_session, body_measurement, heart_rate_sample, glucose_sample). |
providerRecordId |
The store's own id — the HealthKit object UUID, the Health Connect metadata.id (for a day's summary or a heart-rate batch, a stable id you choose, such as 2026-10-20). With the data type it is unique per connection, so uploading the same one again updates the record instead of duplicating it. That also makes the call safe to retry. |
The whole body is validated against the contract: one invalid record refuses the upload
with 422 UNPROCESSABLE_CONTENT (the message says which path). A record whose family the
store does not carry is 400 INVALID_ARGUMENT_ERROR.
Response:
{ "data": { "connectionId": "conn_…", "created": 1, "updated": 0, "unchanged": 0 } }What happens on upload
- The first upload of a member for a store creates its connection — no OAuth, no tokens;
the device stands for the account (
providerUserId: "device:<memberId>"). It counts towards the plan's connected-members limit (403 PLAN_LIMIT_REACHEDwhen full) and sendsconnection.created. - Each upload marks the connection synced and runs the same writer as provider syncs: new
records raise
*.created, changed ones*.updated, identical ones count asunchanged. - The data then reads like any provider's:
GET /v1/members/{id}/workouts?provider=apple_health.
Mapping the stores (guidance)
The upload carries normalized records, so the mapping happens in your app. Latido's units
are fixed: meters, seconds, kg, kcal, bpm, ms (HRV RMSSD), mg/dL, °C, percentages on 0-100.
Instants are ISO 8601; send them with the phone's offset and set utcOffsetMinutes, so
the member's wall clock is kept.
| Data | HealthKit | Health Connect |
|---|---|---|
| Workout | HKWorkout (+ HKStatistics for heart rate) |
ExerciseSessionRecord + aggregates over the session (distance, calories, heart rate) |
| Sleep | HKCategoryValueSleepAnalysis samples grouped into one night per record (stages) |
SleepSessionRecord with stages |
| Daily summary | HKStatisticsCollectionQuery per local day |
aggregateGroupByPeriod per local day |
| Body | BodyMass, BodyFatPercentage, Height, BodyMassIndex, BodyTemperature, RestingHeartRate, blood-pressure correlation |
WeightRecord, HeightRecord, BodyFatRecord, BoneMassRecord, BloodPressureRecord, BodyTemperatureRecord, RestingHeartRateRecord |
| Heart rate | quantity samples batched into one series (samples: [{ t, bpm }], t = seconds since startTime) |
HeartRateRecord.samples |
| Glucose | BloodGlucose (mmol/L × 18.0156 → mg/dL) |
BloodGlucoseRecord.level.inMilligramsPerDeciliter |
Unit traps: HealthKit percentages are 0-1 fractions (× 100); energy in kJ ÷ 4.184; Apple's HRV
is SDNN — do not put it in hrvRmssdMs (Health Connect's
HeartRateVariabilityRmssdRecord is RMSSD and fits). Keep the writing app in
source.appName (bundle id / package name) and the device in source.deviceName /
deviceManufacturer, so a Garmin workout synced through Apple Health can be told from an
Apple Watch one.
Not supported yet
- Raw uploads. The endpoint accepts only the normalized
{ provider, records }body described on this page. A raw format (HealthKit / Health Connect objects converted on the server) is planned for the native SDKs; it is not something the API accepts today. - Deletions. A record deleted on the phone cannot be uploaded as a deletion yet. To
remove a member's data, delete the member (
DELETE /v1/members/{memberId}).