curl --request PUT \
--url https://web-api.dev.soxai.site/api/groups/{group_id}/reactions/{target_uid}/{emoji_id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://web-api.dev.soxai.site/api/groups/{group_id}/reactions/{target_uid}/{emoji_id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.put(url, headers=headers)
print(response.text)const options = {method: 'PUT', headers: {Authorization: 'Bearer <token>'}};
fetch('https://web-api.dev.soxai.site/api/groups/{group_id}/reactions/{target_uid}/{emoji_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"targetUid": "user456",
"day": "2026-10-14",
"reactions": [
{
"id": "muscle",
"emoji": "💪",
"count": 2,
"mine": true,
"uids": [
"user123",
"user789"
]
},
{
"id": "party",
"emoji": "🎉",
"count": 1,
"mine": false,
"uids": [
"user789"
]
}
]
}{
"error": "bad-request",
"error_msg": "unknown reaction: fire"
}{
"error": "http-error",
"error_msg": "Invalid authentication credentials. <reason>"
}{
"error": "forbidden",
"error_msg": "Not enough permission"
}{
"error": "not-found",
"error_msg": "member not found"
}{
"error": "conflict",
"error_msg": "at most 3 reactions per member; remove one first"
}{
"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>"
}Send a reaction to a member
A request authenticated with a Sandbox soxai-api-key is answered 403 This endpoint is not available in the sandbox., because it changes state that every Sandbox caller shares. It is published here because the Sandbox runs the same build; use it in Production. See Sandbox.
Send one reaction to one member of the group (SOX-116).
Idempotent, and deliberately not a toggle. Sending the same reaction twice
leaves exactly one; use DELETE to take it back. A toggle would not survive a
retry — a resend on a flaky connection would undo the reaction the user had just
sent, and a tap on a family screen is exactly the kind of call that gets retried.
Reactions are per day: every day starts from zero, and a
reaction belongs to the day it was sent on. count is therefore “how many members
hold this reaction on this person on day”. Omit day_str for today; pass an
earlier day_str to read (or amend) that day’s reactions, which stay stored.
“Today” is the JST calendar day (UTC+09:00) for every member of the group, not
each member’s own local day, so a reaction lands in the same bucket whoever sends
or reads it. A client that wants its own local day passes it as day_str. The
response echoes the resolved day as day.
Reactions are not health data and are not masked. A member who has hidden
their metrics (masterHidden) shows an empty data card and still sends and
receives reactions — the same line joinedAt sits on: sharing settings govern
health values, while who is in a group and what they send each other is already
visible to everyone in it. Nothing here is written to the group’s audit log,
because nothing here reads a health value.
uids lists the members currently holding that reaction. Every one of them is a
member of this group, whose roster the caller can already read in full, so this
discloses nothing new — it is what lets a client draw “Dad and Mom cheered you”
without a second call. Actors who have since left the group are filtered out, so
a count never includes somebody who is no longer here.
The list is ordered by the catalog, not by count: ordering by count makes the
row of icons jump under the user’s finger at the moment they tap one. An emoji
nobody holds is omitted, and a member with no reactions at all gets [].
Parameters
group_id(path, required) - the group both members belong to.target_uid(path, required) - the member receiving the reaction. Sending one to yourself is allowed.emoji_id(path, required) - a reaction id from the catalog (eyes,muscle,smile,surprise,party,heart), not the emoji character itself. The response carries a reference character; each app renders its own artwork for an id.day_str(query, optional) - the day the reaction belongs to,YYYY-MM-DD. Defaults to today (JST).
Response
200returns the member’s full reaction list ondayas it now stands — the same shapeDELETEreturns, so one parser serves both.
Errors
| Status | Condition |
|---|---|
| 400 | emoji_id is not in the catalog, or day_str is not a YYYY-MM-DD date or is a day that has not begun anywhere yet |
| 404 | target_uid is not on this group’s roster |
| 409 | The caller already holds the maximum number of reactions on this member that day |
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.
curl --request PUT \
--url https://web-api.dev.soxai.site/api/groups/{group_id}/reactions/{target_uid}/{emoji_id} \
--header 'Authorization: Bearer <token>'import requests
url = "https://web-api.dev.soxai.site/api/groups/{group_id}/reactions/{target_uid}/{emoji_id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.put(url, headers=headers)
print(response.text)const options = {method: 'PUT', headers: {Authorization: 'Bearer <token>'}};
fetch('https://web-api.dev.soxai.site/api/groups/{group_id}/reactions/{target_uid}/{emoji_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"targetUid": "user456",
"day": "2026-10-14",
"reactions": [
{
"id": "muscle",
"emoji": "💪",
"count": 2,
"mine": true,
"uids": [
"user123",
"user789"
]
},
{
"id": "party",
"emoji": "🎉",
"count": 1,
"mine": false,
"uids": [
"user789"
]
}
]
}{
"error": "bad-request",
"error_msg": "unknown reaction: fire"
}{
"error": "http-error",
"error_msg": "Invalid authentication credentials. <reason>"
}{
"error": "forbidden",
"error_msg": "Not enough permission"
}{
"error": "not-found",
"error_msg": "member not found"
}{
"error": "conflict",
"error_msg": "at most 3 reactions per member; remove one first"
}{
"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
The member's reactions as they now stand. Same shape DELETE returns.