curl --request GET \
--url https://web-api.soxai.site/api/v2/GroupView/{group_id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://web-api.soxai.site/api/v2/GroupView/{group_id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://web-api.soxai.site/api/v2/GroupView/{group_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"group": {
"groupId": "b7d1f0c9a2e4d6f8b0c1",
"groupName": "Shibata family",
"status": "active"
},
"members": [
{
"uid": "user123",
"displayName": "Mom",
"role": "admin",
"joinedAt": "2026-08-03T09:30:00+00:00",
"isSelf": true,
"data": {
"sleep_score": 82,
"health_score": 76,
"activity_steps": 8421,
"last_sync_time": "2026-08-28T22:10:00Z"
},
"visibleFields": [
"activity_steps",
"health_score",
"last_sync_time",
"sleep_score"
],
"sharedFrom": "2026-08-03",
"reactions": [
{
"id": "muscle",
"emoji": "👍",
"count": 1,
"mine": false,
"uids": [
"user456"
]
}
],
"sharedFields": {
"sleep_score": true,
"health_score": true,
"activity_score": false,
"sleep_total_time_exclude_awake": false,
"activity_steps": true,
"heat_stroke_severity": false,
"last_sync_time": true
},
"masterHidden": false,
"sharedFieldNames": [
"activity_steps",
"health_score",
"last_sync_time",
"sleep_score"
]
},
{
"uid": "user456",
"displayName": "Dad",
"role": "member",
"joinedAt": "2026-08-05T02:10:00+00:00",
"isSelf": false,
"data": {
"sleep_score": 68,
"heat_stroke_severity": 2,
"heat_stroke_alert_time": "2026-08-28T15:00:00+09:00"
},
"visibleFields": [
"heat_stroke_severity",
"sleep_score"
],
"sharedFrom": "2026-08-05",
"reactions": []
}
]
}{
"error": "bad-request",
"error_msg": "date range must not exceed 366 days"
}{
"error": "http-error",
"error_msg": "Invalid authentication credentials. <reason>"
}{
"error": "forbidden",
"error_msg": "Not enough permission"
}{
"error": "not-found",
"error_msg": "group not found"
}{
"error": "unprocessable-entity",
"error_msg": "body.<field>: <validation message>"
}{
"error": "rate-limited",
"error_msg": "Rate limit exceeded. Retry after <n> seconds."
}{
"error": "internal-server-error",
"error_msg": "Failed to fetch data: <error_message>"
}Get a family group's shared health data
The single data-view endpoint for family groups (spec §3.5). All masking goes through resolve_field_mask — no endpoint-local mask logic.
Every card is scoped to the days that member has been in the group (SOX-91):
nothing from before their joinedAt day is shared, whatever the requested
range says, and leaving and rejoining moves the floor to the rejoin day
(leaving deletes the membership, so a rejoin gets a fresh joinedAt). So a
long start_day does not reach further back than the join for a recent
member — their history simply begins on their join day — and a member who
joined today shows only today. Days are the owner’s local days, and each card
reports its own floor as sharedFrom so a short history reads as a recent
join rather than as missing data.
data holds the latest visible value per catalog metric; last_sync_time
(UTC ISO 8601) is a profile snapshot, so it appears there only and never in
the per-day history.
Sleep duration (睡眠時間) is sleep_total_time_exclude_awake: minutes of true
sleep (純睡眠時間), that is sleep with mid-sleep waking (中途覚醒) excluded - the
same number the owner’s own sleep screen shows (SOX-95). It used to be
sleep_total_time (total sleep time, 総睡眠時間), which reads longer by
exactly the night’s awake time, so one label answered two different values
depending on the screen. BREAKING for clients on the old key — both the
response key and the sharedFields opt-in key are renamed, because a
GroupView catalog key is always the stored InfluxDB _field name. DailyInfo
v2 serves this same field to the personal screen as sleep_total_sleep_time.
A night with no true sleep time (純睡眠時間) recorded omits the field rather
than reporting 0 — the same ”—” the owner sees for that night, since there is
no total to fall back to that would still agree with their own screen.
Beside displayName and role, each card carries joinedAt (SOX-92) — when
that person’s membership started, as offset-carrying ISO 8601 like the group’s
own createdAt. It sits outside data and outside visibleFields because it
is membership metadata rather than a health value: sharing settings do not hide
who is in the group. Same key, same value as the members rows of
GET /api/groups.
joinedAt and sharedFrom are not duplicates: the first is the exact instant
the membership began, the second the owner’s LOCAL day that instant falls in.
They disagree whenever the two calendars do — a JST member who joined at
02:00 local has joinedAt on the previous UTC day. Read joinedAt for the join
date (参加日) and ordering, sharedFrom for where that member’s data starts.
heat_stroke_severity is the stored severity integer, passed through
unchanged (SOX-83): 0=NORMAL, 1=Low, 2=Medium, 3=High, 4=Critical — the same
scale the apps already render on their own heat-stroke screens. Per day it is
the HIGHEST severity that day reached, not the most recent one. It comes with
heat_stroke_alert_time, the last moment that peak was reached, as ISO 8601
in the OWNER’s UTC offset (2026-08-28T15:00:00+09:00) so the wall clock
reads the same for a viewer in another timezone. A severity-0 day carries no
alert time: NORMAL is not an alert (警戒).
The pair is omitted entirely when there is no alert record in the window,
which is NOT the same statement as 0/NORMAL: soxai-app only writes a record
when it raises an alert, so an absent field means “no alert, or no ring, or
no sync”. Pair it with last_sync_time to tell those apart. Its history
days are the OWNER’s local days, since the underlying alerts are timestamped
instants rather than the day labels the daily metrics use.
Each card carries reactions — the emoji the rest of the group is currently
holding on that member (SOX-116), with a derived count and mine for the
caller. They are per day and reset daily: the cards carry today’s (JST)
reactions, or those of the day named by day_str, and the response reports
which day as reactionDay. day_str is independent of start_day/end_day,
which govern health data only. They sit outside data and outside the
mask for the same reason joinedAt does — a reaction is a message between
members, not a health value — so a masterHidden member shows an empty data
and still carries reactions, and reading them writes no audit entry. [] when
nobody has reacted. Send and take back with
PUT/DELETE /api/groups/{group_id}/reactions/{target_uid}/{emoji_id}, or read
them on their own with GET /api/groups/{group_id}/reactions when the full
InfluxDB fan-out this endpoint pays for is not needed.
visibleFields is the viewer’s mask. Since SOX-83 that INCLUDES the caller’s
own card: your own card shows exactly what the rest of the group sees of you,
so the family screen doubles as a preview of your sharing settings. An owner
with masterHidden therefore sees their own card empty too. The self card
also carries sharedFields / masterHidden / sharedFieldNames — the raw
toggles the edit screen needs (SOX-67);
GET /api/groups/{group_id}/members/me returns the same.
Parameters
group_id(path, required) - the group whose members’ cards are returned.start_day/end_day(query, optional) -YYYY-MM-DD, both inclusive. Give either one and every card also carries a per-dayhistory; omit both for latest values only.day_str(query, optional) -YYYY-MM-DD, the day whosereactionsthe cards carry. Defaults to today (JST). Reactions only: it does not affectdataorhistory.
Response
200returns one card per active member, each holdingdata- the latest visible value per catalog metric - andvisibleFields, what that member shares with the caller. A member sharing nothing still appears, with an emptydata.- The caller’s own card carries the whole catalog regardless of what they share.
Errors
| Status | Condition |
|---|---|
| 400 | start_day, end_day or day_str is not YYYY-MM-DD, start_day is after end_day, or the range exceeds 366 days |
| 404 | The group does not exist |
Authorization
- Scheme:
Authorization: Bearer <idToken>orsoxai-api-key: <api key>. - Authority inside a group comes from the caller’s own membership record, never from a SOXAI
role: an
Admin,PartnerAdminorOrgAdminclaim grants nothing here. - Required: an active member of the group. A caller who is not one receives
403 not a member of this group, whether or not the group exists.
Response fields
GroupView
| Field | Unit | Description |
|---|---|---|
sleep_score | pt | Sleep Score: Overall index of sleep quality |
health_score | pt | Health Score: Overall index of physical health, ranging from 0 to 100 |
activity_score | pt | Activity Score: Index of physical activity |
sleep_total_time_exclude_awake | mins | Sleep - Sleep Time Excluding Awake Time: Sleep duration excluding time spent awake |
activity_steps | step | Activity - Steps: Number of steps in a day |
heat_stroke_severity | code | Heat Stroke Alert - Severity: Integer from 0 to 4, passed through from SX_HeatStroke_Alert. Absent when no alert record exists for the day, which covers no alert, no ring and no sync alike |
heat_stroke_alert_time | datetime | Heat Stroke Alert - Alert Time: ISO 8601 instant the alert fired, carrying the local UTC offset of the owner. Shares the heat_stroke_severity sharing setting |
last_sync_time | datetime | Last Sync Time: UTC ISO 8601 timestamp of the last recorded ring sync, to second precision. Read from the Firestore user profile rather than a time series, and absent when the user has never synced |
curl --request GET \
--url https://web-api.soxai.site/api/v2/GroupView/{group_id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://web-api.soxai.site/api/v2/GroupView/{group_id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://web-api.soxai.site/api/v2/GroupView/{group_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"group": {
"groupId": "b7d1f0c9a2e4d6f8b0c1",
"groupName": "Shibata family",
"status": "active"
},
"members": [
{
"uid": "user123",
"displayName": "Mom",
"role": "admin",
"joinedAt": "2026-08-03T09:30:00+00:00",
"isSelf": true,
"data": {
"sleep_score": 82,
"health_score": 76,
"activity_steps": 8421,
"last_sync_time": "2026-08-28T22:10:00Z"
},
"visibleFields": [
"activity_steps",
"health_score",
"last_sync_time",
"sleep_score"
],
"sharedFrom": "2026-08-03",
"reactions": [
{
"id": "muscle",
"emoji": "👍",
"count": 1,
"mine": false,
"uids": [
"user456"
]
}
],
"sharedFields": {
"sleep_score": true,
"health_score": true,
"activity_score": false,
"sleep_total_time_exclude_awake": false,
"activity_steps": true,
"heat_stroke_severity": false,
"last_sync_time": true
},
"masterHidden": false,
"sharedFieldNames": [
"activity_steps",
"health_score",
"last_sync_time",
"sleep_score"
]
},
{
"uid": "user456",
"displayName": "Dad",
"role": "member",
"joinedAt": "2026-08-05T02:10:00+00:00",
"isSelf": false,
"data": {
"sleep_score": 68,
"heat_stroke_severity": 2,
"heat_stroke_alert_time": "2026-08-28T15:00:00+09:00"
},
"visibleFields": [
"heat_stroke_severity",
"sleep_score"
],
"sharedFrom": "2026-08-05",
"reactions": []
}
]
}{
"error": "bad-request",
"error_msg": "date range must not exceed 366 days"
}{
"error": "http-error",
"error_msg": "Invalid authentication credentials. <reason>"
}{
"error": "forbidden",
"error_msg": "Not enough permission"
}{
"error": "not-found",
"error_msg": "group not found"
}{
"error": "unprocessable-entity",
"error_msg": "body.<field>: <validation message>"
}{
"error": "rate-limited",
"error_msg": "Rate limit exceeded. Retry after <n> seconds."
}{
"error": "internal-server-error",
"error_msg": "Failed to fetch data: <error_message>"
}Authorizations
Path Parameters
Query Parameters
Response
One card per active member. A member sharing nothing still appears, with an empty data. Cards carry history and the response carries range only when a date is given.