Endpoints
Reference
API reference
Source: server/api/openapi.json on GitHub · edit this page
Base URL
http://127.0.0.1:8081 on the server, or your HTTPS proxy’s URL.
Authentication
Authorization: Bearer $PULS_API_TOKEN on every /v1 route.
Conventions
Epoch-millisecond timestamps, half-open [start, end) ranges, days in PULS_TIME_ZONE. Errors are {"error": "…"}.
Specification
openapi.jsonOpenAPI 3.1 · v1.0.0 · 18 endpoints
New to the API? Start with the overview: reaching the service, choosing the user, paging, errors and rate limits.
Discovery
Unauthenticated endpoints that describe the service and report its health.
A JSON index pointing at the documentation, the OpenAPI document and the health check.
Responses
- 200
The index.
Response fields
namestringversionstringdocsstringopenapistringhealthstringauthstringuserstringdescriptionstring
curl "http://127.0.0.1:8081/"{
"name": "PulsHealth Product API",
"version": "v1",
"docs": "/docs",
"openapi": "/openapi.json",
"health": "/healthz",
"auth": "Authorization: Bearer $PULS_API_TOKEN",
"user": "Optional ?user=<uuid> on every /v1 route selects the user (default PULS_USER_ID; others need PULS_MULTI_USER=true); GET /v1/users lists them.",
"description": "Read-only API for downstream products that use PulsHealth data."
}A self-contained HTML reference served by the deployment itself. The full documentation is at https://pulshealth.com/docs/api/.
Responses
- 200
HTML reference.
text/html
curl "http://127.0.0.1:8081/docs"This document. servers[0].url is the base URL the request arrived on, so a client generator or a ChatGPT Action imports it as is. Behind a proxy that host comes from X-Forwarded-Host only with TRUST_PROXY_HEADERS=true.
Responses
- 200
The OpenAPI 3.1 document.
application/openapi+json
curl "http://127.0.0.1:8081/openapi.json"Liveness for a container health check. The database status is cached for two seconds, so polling it never holds a database connection.
Responses
- 200
The service and its database are up.
- 503
The database did not answer.
Response fields
okbooleandbbooleanWhether the database answered a ping in the last two seconds.
curl "http://127.0.0.1:8081/healthz"{
"ok": true,
"db": true
}Users
Who the deployment answers for, and their profile.
Every user with the gate on (PULS_MULTI_USER=true); only the default user with it off. default is the user served when a request names none (PULS_USER_ID); multiUser says whether ?user= may name anyone else. timeZone is the zone every local day in this API's answers is cut in.
Responses
- 200
Users.
- 400
A parameter is missing, malformed or out of range. The message names it.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
usersarray of UseruserIDstring (uuid)The user's id. Pass it as
user=on any/v1route.namestring | nullName from the phone's profile, if set.
emailstring | nullE-mail from the phone's profile, if set.
createdAtinteger (int64)When the server first saw this user. Epoch milliseconds (UTC).
lastSyncinteger (int64) | nullEpoch milliseconds of the most recent batch; null when nothing has been uploaded.
batchesinteger (int64)Upload batches received.
uploadedSamplesinteger (int64)Sum of the sample counts the batches declared.
defaultstring (uuid)The user served when a request names none (
PULS_USER_ID).multiUserbooleanWhether
user=may name anyone else (PULS_MULTI_USER).timeZonestringThe IANA zone (
PULS_TIME_ZONE) every local calendar day this API answers with is cut in;UTCwhen unset. The API refuses to start when it disagrees with the database's zone.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/users"{
"users": [
{
"userID": "5ea4d000-0000-4000-8000-000000000001",
"name": "Alex Example",
"email": "alex@example.com",
"createdAt": 1767225600000,
"lastSync": 1767225600000,
"batches": 1824,
"uploadedSamples": 2391044
}
],
"default": "5ea4d000-0000-4000-8000-000000000001",
"multiUser": false,
"timeZone": "Europe/Berlin"
}The HealthKit characteristics the phone uploaded for this user: name, e-mail, date of birth and biological sex.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.
Responses
- 200
Profile.
- 400
A parameter is missing, malformed or out of range. The message names it.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 404
Nothing with that identifier exists for this user.
- 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
userIDstring (uuid)The user's id.
namestring | nullName from the phone's profile, if set.
emailstring | nullE-mail from the phone's profile, if set.
dateOfBirthinteger (int64) | nullDate of birth as epoch milliseconds at UTC midnight.
biologicalSexstring | nullHealthKit's biological sex, e.g.
female,male,other.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/profile"{
"userID": "5ea4d000-0000-4000-8000-000000000001",
"name": "Alex Example",
"email": "alex@example.com",
"dateOfBirth": 1767225600000,
"biologicalSex": "female"
}Catalog
Which HealthKit types hold data, with row counts and time spans.
Every type with at least one row for this user, raw or aggregate, with its kind, canonical unit, row counts and time span. Counting rows is expensive, so the answer is cached per user for five minutes. After that the cached answer is still served, for up to an hour, while one background refresh replaces it, so new uploads can take a little longer than five minutes to appear; only a user's first request waits for the count.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.
Responses
- 200
Catalog.
- 400
A parameter is missing, malformed or out of range. The message names it.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
typesarray of CatalogTypeidentifierstringHealthKit identifier.
kindstringquantity,category,workout,heartbeatSeries,ecg,stateOfMind,medicationDoseoractivitySummary.unitstring | nullCanonical unit every value of the type is stored in; null for types without one.
rowsinteger (int64)rawRowsplusaggregateRows.rawRowsinteger (int64)Individual samples stored.
aggregateRowsinteger (int64)Aggregate buckets the phone computed (what daily metrics are built from).
earliestinteger (int64) | nullStart of the oldest row. Epoch milliseconds (UTC).
latestinteger (int64) | nullStart of the newest row. Epoch milliseconds (UTC).
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/catalog/types"{
"types": [
{
"identifier": "HKQuantityTypeIdentifierStepCount",
"kind": "quantity",
"unit": "count",
"rows": 184211,
"rawRows": 171032,
"aggregateRows": 13179,
"earliest": 1767225600000,
"latest": 1767229200000
}
]
}Metrics
Latest readings and one-value-per-day series, deduplicated across devices.
The newest raw sample of each requested quantity type. Types with no data are left out, so the list can be shorter than types.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.typesstringqueryrequiredComma-separated HealthKit quantity identifiers, at most 50.
Responses
- 200
Latest metrics.
- 400
A parameter is missing, malformed or out of range. The message names it.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
metricsarray of LatestMetricidentifierstringHealthKit identifier.
unitstring | nullCanonical unit of value.
valuenumber | nullThe reading.
timestampinteger (int64)Start of the sample. Epoch milliseconds (UTC).
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/metrics/latest?types=HKQuantityTypeIdentifierStepCount"{
"metrics": [
{
"identifier": "HKQuantityTypeIdentifierRestingHeartRate",
"unit": "count/min",
"value": 54,
"timestamp": 1767225600000
}
]
}One value per local calendar day (in the server's PULS_TIME_ZONE) for each requested type, every day overlapping [start, end). Paged in days across the requested types: limit and offset count day rows, not metrics, in the order the response nests them (the requested types in request order, each ascending by day); nextOffset is offset plus the rows on the page and a page shorter than limit is the last. The default page holds a year of 27 types.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.typesstringqueryrequiredComma-separated HealthKit identifiers, at most 50; the response keeps this order.
startinteger (int64)queryrequiredInclusive start of the range, in epoch milliseconds.
endinteger (int64)queryrequiredExclusive end of the range, in epoch milliseconds. Must be after
start.limitintegerqueryDay rows per page, across all requested types; larger values are clamped.
Default:
10000· Maximum:50000offsetintegerqueryRows to skip. Pass the previous page's
nextOffset.Default:
0
Responses
- 200
Daily metrics.
- 400
Missing types or range, or an invalid
limitoroffset. - 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
metricsarray of DailyMetricidentifierstringHealthKit identifier.
unitstring | nullCanonical unit of every value.
daysarray of objectDays with a value, ascending.
datestring (date)Local calendar day (
PULS_TIME_ZONE).valuenumber | nullThe day's total for a cumulative type (steps), its mean for a discrete one (heart rate).
nextOffsetintegerOffset to pass for the next page; a page with fewer day rows than
limitmeans the end.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/metrics/daily?types=HKQuantityTypeIdentifierStepCount&start=1735689600000&end=1738368000000"{
"metrics": [
{
"identifier": "HKQuantityTypeIdentifierStepCount",
"unit": "count",
"days": [
{
"date": "2026-01-01",
"value": 8412
}
]
}
],
"nextOffset": 31
}Activity
Apple Activity rings, one row per local calendar day.
One row per local calendar day overlapping [start, end), as the Fitness app shows the rings.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.startinteger (int64)queryrequiredInclusive start of the range, in epoch milliseconds.
endinteger (int64)queryrequiredExclusive end of the range, in epoch milliseconds. Must be after
start.
Responses
- 200
Activity days.
- 400
A parameter is missing, malformed or out of range. The message names it.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
daysarray of ActivityDaydatestring (date)Local calendar day.
moveKcalnumber | nullActive energy burned, kcal.
moveGoalKcalnumber | nullMove goal, kcal.
exerciseMinnumber | nullExercise minutes.
exerciseGoalMinnumber | nullExercise goal, minutes.
standHoursnumber | nullHours with a stand.
standGoalHoursnumber | nullStand goal, hours.
moveModeinteger | nullHealthKit move mode: 1 active energy, 2 move time.
moveTimeMinnumber | nullMove minutes (move-time mode only).
moveTimeGoalMinnumber | nullMove goal, minutes (move-time mode only).
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/activity/summary?start=1735689600000&end=1738368000000"{
"days": [
{
"date": "2026-01-01",
"moveKcal": 512.4,
"moveGoalKcal": 600,
"exerciseMin": 34,
"exerciseGoalMin": 30,
"standHours": 11,
"standGoalHours": 12,
"moveMode": 1,
"moveTimeMin": null,
"moveTimeGoalMin": null
}
]
}Workouts
Workout summaries, detail, and the per-second streams recorded during them.
Workouts newest first, optionally limited to [start, end) on the start time and to one activity type.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.startinteger (int64)queryOnly workouts starting at or after this instant. Epoch milliseconds.
endinteger (int64)queryOnly workouts starting before this instant. Epoch milliseconds.
activityTypestringqueryOnly this HealthKit activity type, e.g.
running.limitintegerqueryWorkouts per page; larger values are clamped.
Default:
50· Maximum:200offsetintegerqueryRows to skip. Pass the previous page's
nextOffset.Default:
0
Responses
- 200
Workouts.
- 400
An invalid range, limit or offset.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
workoutsarray of WorkoutSummaryuuidstring (uuid)HealthKit UUID.
activityTypestringHealthKit activity type name.
startinteger (int64)Start. Epoch milliseconds (UTC).
endinteger (int64)End. Epoch milliseconds (UTC).
durationSnumber | nullDuration, seconds.
distanceMnumber | nullTotal distance, metres; null when none was recorded.
energyKcalnumber | nullActive energy, kcal.
hasRoutebooleanWhether GPS route points were synced.
availableMetricsarray of stringHealthKit identifiers with a stream in
GET /v1/workouts/{uuid}/series.nextOffsetintegerOffset to pass for the next page; a page shorter than
limitis the last.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/workouts"{
"workouts": [
{
"uuid": "5ea4d000-0000-4000-8000-000000000001",
"activityType": "running",
"start": 1767225600000,
"end": 1767229200000,
"durationS": 2712.5,
"distanceM": 8046.7,
"energyKcal": 612.3,
"hasRoute": true,
"availableMetrics": [
"HKQuantityTypeIdentifierHeartRate",
"HKQuantityTypeIdentifierRunningPower"
]
}
],
"nextOffset": 50
}One workout with HealthKit's per-type statistics, its events and, for a multisport workout, its activities.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.uuidstring (uuid)pathrequiredThe workout's HealthKit UUID, from
GET /v1/workouts.
Responses
- 200
Workout detail.
- 400
Invalid UUID.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 404
Nothing with that identifier exists for this user.
- 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
uuidstring (uuid)HealthKit UUID.
activityTypestringHealthKit activity type name.
startinteger (int64)Start. Epoch milliseconds (UTC).
endinteger (int64)End. Epoch milliseconds (UTC).
durationSnumber | nullDuration, seconds.
distanceMnumber | nullTotal distance, metres; null when none was recorded.
energyKcalnumber | nullActive energy, kcal.
hasRoutebooleanWhether GPS route points were synced.
availableMetricsarray of stringHealthKit identifiers with a stream in
GET /v1/workouts/{uuid}/series.statisticsDetailmap of objectPer-type statistics HealthKit computed for the workout, keyed by HealthKit identifier, in each type's canonical unit.
eventsarray of objectWorkout events (pauses, laps, segments) as HealthKit reported them.
activitiesarray of objectSub-activities of a multisport workout.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/workouts/5ea4d000-0000-4000-8000-000000000001"{
"uuid": "5ea4d000-0000-4000-8000-000000000001",
"activityType": "running",
"start": 1767225600000,
"end": 1767229200000,
"durationS": 2712.5,
"distanceM": 8046.7,
"energyKcal": 612.3,
"hasRoute": true,
"availableMetrics": [
"HKQuantityTypeIdentifierHeartRate",
"HKQuantityTypeIdentifierRunningPower"
],
"statisticsDetail": {
"HKQuantityTypeIdentifierHeartRate": {
"min": 92,
"avg": 151.6,
"max": 178
},
"HKQuantityTypeIdentifierActiveEnergyBurned": {
"sum": 612.3
}
},
"events": [
{
"type": "pause",
"start": 1767226800000,
"end": 1767226860000
}
],
"activities": []
}Per-second curves recorded during the workout, each downsampled to at most maxPoints points by bucket-averaging while keeping the first and last point.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.uuidstring (uuid)pathrequiredThe workout's HealthKit UUID, from
GET /v1/workouts.typesstringqueryComma-separated HealthKit identifiers, at most 50; omit for every recorded stream.
maxPointsintegerqueryPoints per stream after downsampling; larger values are clamped.
Default:
500· Maximum:5000
Responses
- 200
Workout series.
- 400
Invalid UUID or parameters.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 404
Nothing with that identifier exists for this user.
- 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
uuidstring (uuid)startinteger (int64)Workout start. Epoch milliseconds (UTC).
endinteger (int64)Workout end. Epoch milliseconds (UTC).
maxPointsintegerThe point cap applied to each stream.
seriesarray of WorkoutSeriestypestringHealthKit identifier of the stream.
unitstring | nullCanonical unit of every value.
totalPointsintegerPoints recorded before downsampling.
pointsarray of [integer (int64), number][epoch milliseconds, value] pairs, ordered by time.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/workouts/5ea4d000-0000-4000-8000-000000000001/series"{
"uuid": "5ea4d000-0000-4000-8000-000000000001",
"start": 1767225600000,
"end": 1767229200000,
"maxPoints": 500,
"series": [
{
"type": "HKQuantityTypeIdentifierHeartRate",
"unit": "count/min",
"totalPoints": 2713,
"points": [
[
1767225600000,
142
]
]
}
]
}Sleep
Sleep sessions, attributed to the wake-up day, with stage minutes.
One row per sleep session, attributed to the local calendar day it ended on. Sessions are split on gaps over three hours, and every local day overlapping [start, end) is covered.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.startinteger (int64)queryrequiredInclusive start of the range, in epoch milliseconds.
endinteger (int64)queryrequiredExclusive end of the range, in epoch milliseconds. Must be after
start.
Responses
- 200
Sleep nights.
- 400
Invalid range, or a range over 366 days.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
nightsarray of SleepNightdatestring (date)Local wake-up day.
startinteger (int64)First sample of the session, any source. Epoch milliseconds (UTC).
endinteger (int64)Last sample of the session, any source. Epoch milliseconds (UTC).
inBedMinutesnumberHighest single-source in-bed total, minutes.
asleepMinutesnumberCore + deep + REM + unspecified of the winning source, minutes.
stagesSleepStagesMinutes per stage, from the single source that recorded the most sleep.
asleepMinutesis core + deep + rem + unspecified; awake is time awake during the session and is not part of it.corenumberdeepnumberremnumberunspecifiednumberawakenumbersourcesintegerDistinct sources that contributed samples to this session.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/sleep/daily?start=1735689600000&end=1738368000000"{
"nights": [
{
"date": "2026-01-01",
"start": 1767221400000,
"end": 1767249300000,
"inBedMinutes": 462,
"asleepMinutes": 431.5,
"stages": {
"core": 238,
"deep": 71.5,
"rem": 122,
"unspecified": 0,
"awake": 18
},
"sources": 2
}
]
}Samples
Individual HealthKit records of one type, exactly as synced.
Individual HealthKit records, ordered by start time, not deduplicated across devices. The range is [start, end) on the sample start time and may not exceed 31 days.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.typestringqueryrequiredExactly one HealthKit identifier (see
/v1/catalog/types).startinteger (int64)queryrequiredInclusive start of the range, in epoch milliseconds.
endinteger (int64)queryrequiredExclusive end of the range, in epoch milliseconds. Must be after
start.limitintegerquerySamples per page; larger values are clamped.
Default:
1000· Maximum:5000offsetintegerqueryRows to skip. Pass the previous page's
nextOffset.Default:
0
Responses
- 200
Samples.
- 400
Unknown type, a non-sample type, or a range over 31 days.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
typestringThe requested HealthKit identifier.
kindstringunitstring | nullCanonical unit of every quantity value; null for category types.
samplesarray of Sampleuuidstring (uuid)HealthKit UUID.
startinteger (int64)Start. Epoch milliseconds (UTC).
endinteger (int64)End. Epoch milliseconds (UTC).
valuenumber | nullQuantity value in the page's canonical unit, or the category type's integer enum value.
labelstring | nullCategory types only: the HealthKit name of value.
sourcestring | nullName of the app or device that recorded it.
nextOffsetintegerOffset to pass for the next page; a short page means the end.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/samples?type=HKQuantityTypeIdentifierStepCount&start=1735689600000&end=1738368000000"{
"type": "HKQuantityTypeIdentifierHeartRate",
"kind": "quantity",
"unit": "count/min",
"samples": [
{
"uuid": "5ea4d000-0000-4000-8000-000000000001",
"start": 1767225600000,
"end": 1767229200000,
"value": 72,
"label": null,
"source": "Apple Watch"
}
],
"nextOffset": 1000
}State of Mind
Momentary emotions and daily moods logged on iOS 18 and later.
Entries whose instant falls on a local calendar day overlapping [start, end), oldest first. At most 366 days per request.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.startinteger (int64)queryrequiredInclusive start of the range, in epoch milliseconds.
endinteger (int64)queryrequiredExclusive end of the range, in epoch milliseconds. Must be after
start.
Responses
- 200
Entries.
- 400
Invalid range, or a range over 366 days.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
entriesarray of StateOfMindEntryuuidstring (uuid)datestring (date)Local calendar day of the entry.
timestampinteger (int64)When it was logged. Epoch milliseconds (UTC).
kindstringmomentaryEmotion or dailyMood.
valencenumber | null-1 (very unpleasant) to +1 (very pleasant).
valenceClassificationstring | nullApple's band for valence, e.g. slightlyPleasant.
labelsarray of stringFeelings picked, e.g. calm, stressed.
associationsarray of stringWhat they are about, e.g. work, family.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/state-of-mind?start=1735689600000&end=1738368000000"{
"entries": [
{
"uuid": "5ea4d000-0000-4000-8000-000000000001",
"date": "2026-01-01",
"timestamp": 1767225600000,
"kind": "momentaryEmotion",
"valence": 0.42,
"valenceClassification": "slightlyPleasant",
"labels": [
"calm",
"content"
],
"associations": [
"work"
]
}
]
}Summary
Recent data as one short page for a chat without an MCP connection.
The last range calendar days (7d by default; ending today in the server's PULS_TIME_ZONE) as one markdown page of under sixty lines, meant to be pasted into a chat that has no MCP connection: a header naming the user, the days and the zone, then a section for each kind of data that exists — activity (steps, active energy, exercise minutes, stand hours as daily means and totals), heart (resting heart rate, HRV), sleep (time asleep per night), workouts (count, total time, distance, most frequent activities), body (newest weight and body fat) — and a coverage line (last sync, days with data, and the reminder that daily figures are already deduplicated across devices). Every figure comes from the daily surfaces the other endpoints serve, never from raw samples. format=json returns the same numbers as a Summary object.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.rangestringqueryHow many calendar days, ending today, the summary covers.
One of:
7d,14d,30d,90d· Default:7dformatstringquerymarkdown for the page (text/markdown), json for the Summary object it is rendered from.
One of:
markdown,json· Default:markdown
Responses
- 200
The summary.
text/markdown
- 400
A range or format outside the accepted values.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 504
The query ran past its 30-second limit and was cancelled (
{"error": "query took too long; narrow the range"}). Narrow the range or page smaller.
Response fields
userIDstring (uuid)namestring | nullrangestringdaysintegerstartDatestring (date)First local calendar day covered.
endDatestring (date)Last local calendar day covered: today in timeZone.
generatedAtinteger (int64)When the summary was computed. Epoch milliseconds (UTC).
timeZonestringThe IANA zone (
PULS_TIME_ZONE) whose calendar cut the days.activityobjectstepsSummaryStatOne daily series over the summary's range: the days that had a value and the mean, minimum and maximum of those days. total is present for cumulative series only (steps, energy, exercise minutes). source names the table the values came from.
unitstringdaysintegermeannumberminnumbermaxnumbertotalnumbersourcestringactiveEnergySummaryStatOne daily series over the summary's range: the days that had a value and the mean, minimum and maximum of those days. total is present for cumulative series only (steps, energy, exercise minutes). source names the table the values came from.
unitstringdaysintegermeannumberminnumbermaxnumbertotalnumbersourcestringexerciseSummaryStatOne daily series over the summary's range: the days that had a value and the mean, minimum and maximum of those days. total is present for cumulative series only (steps, energy, exercise minutes). source names the table the values came from.
unitstringdaysintegermeannumberminnumbermaxnumbertotalnumbersourcestringstandSummaryStatOne daily series over the summary's range: the days that had a value and the mean, minimum and maximum of those days. total is present for cumulative series only (steps, energy, exercise minutes). source names the table the values came from.
unitstringdaysintegermeannumberminnumbermaxnumbertotalnumbersourcestringheartobjectrestingHeartRateSummaryStatOne daily series over the summary's range: the days that had a value and the mean, minimum and maximum of those days. total is present for cumulative series only (steps, energy, exercise minutes). source names the table the values came from.
unitstringdaysintegermeannumberminnumbermaxnumbertotalnumbersourcestringhrvSDNNSummaryStatOne daily series over the summary's range: the days that had a value and the mean, minimum and maximum of those days. total is present for cumulative series only (steps, energy, exercise minutes). source names the table the values came from.
unitstringdaysintegermeannumberminnumbermaxnumbertotalnumbersourcestringsleepobjectThe longest sleep session of each wake-up day in the range.
nightsintegermeanAsleepMinutesnumberminAsleepMinutesnumbermaxAsleepMinutesnumberworkoutsobjectcountintegertotalMinutesnumbertotalDistanceMnumberAbsent when no workout in the range recorded a distance.
byActivityTypearray of objectMost frequent first, at most three.
activityTypestringcountintegerbodyobjectweightSummaryReadingThe newest raw sample of a body metric, whenever it was taken.
valuenumberunitstringtimestampinteger (int64)When the reading was taken. Epoch milliseconds (UTC).
bodyFatSummaryReadingThe newest raw sample of a body metric, whenever it was taken.
valuenumberunitstringtimestampinteger (int64)When the reading was taken. Epoch milliseconds (UTC).
coverageobjectlastSyncinteger (int64) | nullEpoch milliseconds of the most recent upload; null when nothing has been uploaded.
daysWithDataintegerDays in the range on which at least one section has a value.
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/summary"{
"userID": "5ea4d000-0000-4000-8000-000000000001",
"name": "Alex Example",
"range": "7d",
"days": 7,
"startDate": "2026-01-01",
"endDate": "2026-01-07",
"generatedAt": 1767800000000,
"timeZone": "Europe/London",
"activity": {
"steps": {
"unit": "count",
"days": 7,
"mean": 9214,
"min": 4120,
"max": 15873,
"total": 64498,
"source": "metric_daily"
},
"exercise": {
"unit": "min",
"days": 7,
"mean": 38,
"min": 12,
"max": 71,
"total": 266,
"source": "activity_summaries"
}
},
"heart": {
"restingHeartRate": {
"unit": "count/min",
"days": 7,
"mean": 54.3,
"min": 51,
"max": 58,
"source": "metric_daily"
}
},
"sleep": {
"nights": 7,
"meanAsleepMinutes": 428.6,
"minAsleepMinutes": 371,
"maxAsleepMinutes": 489
},
"workouts": {
"count": 4,
"totalMinutes": 197.5,
"totalDistanceM": 31420,
"byActivityType": [
{
"activityType": "running",
"count": 3
},
{
"activityType": "yoga",
"count": 1
}
]
},
"body": {
"weight": {
"value": 71.8,
"unit": "kg",
"timestamp": 1767690000000
}
},
"coverage": {
"lastSync": 1767799000000,
"daysWithData": 7
}
}Export
Whole ranges streamed as CSV or JSONL files.
Streams a whole range as a file (Transfer-Encoding: chunked, Content-Disposition: attachment) instead of a JSON document. CSV opens the file with a header row; JSONL writes one JSON object per line whose keys are the same column names. Field names match the JSON endpoints; where an endpoint nests (a metric's days, a night's stages) the export flattens, repeating the identifying fields on every row and naming a nested field by its path. Ranges are capped at 31 days for samples (as /v1/samples is) and 366 days for every other dataset — the same cap /v1/sleep/daily and /v1/state-of-mind apply, and deliberately stricter than /v1/metrics/daily and /v1/workouts, which are bounded by a page size rather than by their range, and /v1/activity/summary, which is one small row per day. The daily_metrics and workouts datasets return the whole range (workouts newest first); limit and offset are not used here. At most 2 exports run at once, because each holds a database connection for the length of the download; over that is a 503 with Retry-After. An export ends after 30 minutes, and one whose client stops reading for a minute is dropped; either way the connection is aborted, so the file is a visibly failed download rather than a short one. The 30-second query limit of the other routes does not apply.
Parameters
userstring (uuid)queryThe user to answer for. Defaults to the deployment's
PULS_USER_ID. Naming anyone else needsPULS_MULTI_USER=true, otherwise the answer is403.GET /v1/userslists them.formatstringqueryrequiredcsvfor a spreadsheet (header row first),jsonlfor one JSON object per line.One of:
csv,jsonldatasetstringqueryrequiredWhich rows to export. Each dataset's columns are the matching JSON endpoint's field names.
One of:
daily_metrics,samples,workouts,sleep,activity,state_of_mindstartinteger (int64)queryrequiredInclusive start of the range, in epoch milliseconds.
endinteger (int64)queryrequiredExclusive end of the range, in epoch milliseconds. Must be after
start.typesstringquerydaily_metricsonly, and required there: comma-separated HealthKit identifiers, at most 50.typestringquerysamples only, and required there: exactly one HealthKit identifier.
activityTypestringqueryworkouts only: keep one activity type.
Responses
- 200
The dataset, streamed as an attachment named puls-<dataset>-<start>-<end>.<
csv|jsonl>.text/csv · application/x-ndjson
- 400
Missing or invalid format or dataset, a missing dataset parameter, an unknown type, or a range over the dataset's cap.
- 401
The bearer token is missing or wrong. Counts against the failed-authentication limit.
WWW-Authenticate header
- 403
usernames someone other than the default user whilePULS_MULTI_USERis off. - 429
Too many failed authentications from this client address. The request was refused before the token was checked.
Retry-After header
- 500
The query failed. Details are in the server log, never in the response.
- 503
Too many exports already in progress; retry after the
Retry-Afterinterval.Retry-After header
curl -H "Authorization: Bearer $PULS_API_TOKEN" \
"http://127.0.0.1:8081/v1/export?format=csv&dataset=daily_metrics&start=1735689600000&end=1738368000000"