Will Return / Out of Roam
Roam already has a first-class absence field — Will Return Today (clock
icon, same day) and Out of Roam (calendar icon, multi-day). Members set
it from the Exit menu. user.status.set / .clear write that same field
so an HRIS or calendar integration can keep Roam in sync.
This is not external activity. External
activity is a short-lived glow + emoji with a 60-minute TTL (a live phone
call). Absences belong here: they persist until returnTime or .clear,
and they do not paint a glow or lock the office.
There is no status: "out_of_office" enum. Check-in (checkedIn /
checkedOut) stays read-only. Sending status on .set is rejected.
| Action | Endpoint |
|---|---|
| Record an absence | POST /user.status.set |
| Clear it | POST /user.status.clear |
| Read it back | GET /user.info?expand=status (needs user:read.status) |
| Listen | user.status.update webhook |
Two modes
willReturn.outOfRoam chooses the product surface. Omitted on .set
defaults to true.
| Out of Roam | Will Return Today | |
|---|---|---|
outOfRoam | true (default) | false (must send explicitly) |
returnTime | Future, at most 2 years | Future, less than 10 hours |
| Desktop | Calendar icon on their office | Clock icon |
| Check-in / check-out | Survives. They can be checkedIn with Out of Roam still set | Cleared |
Elapsed returnTime | Server sweep drops it within a minute | Same |
reason is optional, at most 128 Unicode code points ("On vacation",
"Sick leave", "Out to lunch").
A user can be checkedIn and have willReturn (multi-day Out of
Roam). Key off the presence of the object, not status.
Authorization
| Credential | Who you can target | Scopes |
|---|---|---|
| Organization API key / org OAuth | Any active member in the workspace | user:write.status to set/clear. user:read.status to read via user.info / webhooks |
| Personal OAuth | The token owner only | Same scopes — personal-mode OAuth is not exempt |
| Personal Access Token | The token owner only | Write scope check is skipped. Targeting anyone else returns 403 access_denied. user:read.status is still required to read presence |
Unknown, archived, or cross-account users return 404 user_not_found.
Last writer wins with the desktop client: a later Exit-menu save overwrites
the API value, and a later .set overwrites the desktop value. There is
one row per person, not a stack per integration.
Identify the user
userId accepts a bare UUID, a tagged U-… ID, or an ASCII email —
the same convention as group.create members.
HR systems that only have an email do not need a UUID lookup table or a
prior user.info call. Looking a member up by an
address you already have does not require user:read.email. The response
still returns the canonical UUID userId.
Set an absence
curl -X POST https://api.ro.am/v1/user.status.set \
-H "Authorization: Bearer $ROAM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"userId": "ada@example.com",
"willReturn": {
"returnTime": "2026-09-22T09:00:00Z",
"reason": "On vacation",
"outOfRoam": true
}
}'
The 200 body echoes the written willReturn (with outOfRoam defaulted)
plus the user's current status. .set does not check them out, does
not enable Do Not Disturb, and does not change status.
Same-day leave:
"willReturn": {
"returnTime": "2026-09-16T18:00:00Z",
"reason": "Out to lunch",
"outOfRoam": false
}
Re-posting .set replaces the previous absence. There is no heartbeat and
no TTL — keep returnTime accurate, or .clear when they come back early.
Read it back
.set is not a substitute for user.info. To see what colleagues see:
curl -H "Authorization: Bearer $ROAM_TOKEN" \
"https://api.ro.am/v1/user.info?id=0cc74785-e31e-4403-aa5e-0cc7c1897e66&expand=status"
willReturn is omitted without expand=status, without user:read.status,
and after returnTime has elapsed. user.list?expand=status is the same
shape per member.
A user.status.update webhook fires on .set, .clear, desktop edits,
check-in/out, and elapsed sweep. Subscribe with user:read.status. The
payload's data.willReturn is present only while a future absence is set.
Clear
curl -X POST https://api.ro.am/v1/user.status.clear \
-H "Authorization: Bearer $ROAM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"userId": "ada@example.com"
}'
Response is 204 No Content, including when nothing was set. This removes Out of Roam as well as Will Return Today. Call it from your "returned to work" / "leave cancelled" webhook.
Errors
| Code | Status | When |
|---|---|---|
missing_parameter | 400 | userId, willReturn, or willReturn.returnTime missing |
invalid_parameter | 400 | userId is not a UUID/tagged ID/email, returnTime in the past or > 2 years, Will Return Today ≥ 10 hours out, reason over 128 code points |
invalid_arguments | 400 | status sent on .set |
missing_scope | 403 | OAuth / API key lacks user:write.status |
access_denied | 403 | Personal token targeting someone other than the owner |
user_not_found | 404 | Unknown, archived, or cross-account user |
Example: HRIS leave sync
- On
leave.approved(vacation, sick, parental, public holiday),POST /user.status.setwith the employee's email inuserId,returnTime= first working moment after the leave,reasonfrom the leave type, andoutOfRoam: true(or omit it). - If the leave is extended,
.setagain with the newreturnTime. Last writer wins. - On
leave.cancelledor an early return,POST /user.status.clearwith that sameuserId. Treat 204 as success even if they already cleared it in the desktop client.
Do not heartbeat this field with user.activity.set. A 10–60 minute glow
is the wrong product for a week of vacation, and stacking per integration
would fight the single absence the rest of the office sees.