# 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](/docs/guides/user-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`](/docs/api/user-status-set) |
| Clear it | [`POST /user.status.clear`](/docs/api/user-status-clear) |
| Read it back | [`GET /user.info?expand=status`](/docs/api/user-info) (needs `user:read.status`) |
| Listen | [`user.status.update`](/docs/webhooks/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`](/docs/api/group-create) members.
HR systems that only have an email do not need a UUID lookup table or a
prior [`user.info`](/docs/api/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

```bash
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:

```json
"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:

```bash
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

```bash
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

1. On `leave.approved` (vacation, sick, parental, public holiday),
   `POST /user.status.set` with the employee's email in `userId`,
   `returnTime` = first working moment after the leave, `reason` from the
   leave type, and `outOfRoam: true` (or omit it).
2. If the leave is extended, `.set` again with the new `returnTime`. Last
   writer wins.
3. On `leave.cancelled` or an early return, `POST /user.status.clear` with
   that same `userId`. 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.