Users: personal presence and schedules
Your personal presence is the state inbound routing checks first. This page covers the self-service presence endpoints for your own account, the full routing semantics of each state, how external presence federation interacts with what you set yourself, and the troubleshooting paths for a status that does not behave the way you expected. Presence is a per-human-user signal, so these endpoints authenticate with your signed-in session (the same credential the dashboard uses). A server-to-server caller has no concept of “me”, and every read is scoped to the caller — one row per user per request, never another person’s record. Organization-scoped aggregation (the team roster, admin presence reports) is a separate surface; every endpoint here touches only the authenticated user’s row.Decide your state: presence-to-routing semantics
Pick the base state by what you need routing to do, not by how you want to appear — busy and away are cosmetic to the call path, while DND is the only state that physically takes you out of it.
Two cross-cutting rules the table hides:
- DND timing. With no
dnd_untilthe DND is indefinite — it only ends when a non-DND status lands. With adnd_untiltimestamp the cleanup flips you back toavailableat the timestamp. A past-dateddnd_untilis rejected with a 400VALIDATION_ERROR(“dnd_until must be in the future”) so an already-expired window can never masquerade as an active one. - The busy-activity refinement is cosmetic to routing.
in_a_call/in_a_meetingride on top ofbusy(the waydnd_untilcouples todnd) — they render “In a call” / “In a meeting” instead of a bare Busy on the teammate roster, but routing treats the pair exactly like plainbusy: shown unavailable, skipped by chat auto-assignment, still ringable by voice.
Set your presence
status accepts available, busy, away, or dnd, and the refinements below layer only when the status they describe is in force:
Return to available (clearing the refinements in one call):
user.presence.changed realtime event on the tenant channel, and invalidates the inbound-routing DND cache immediately — routing reads the new state on the next lookup. The response always returns your persisted presence, so a busy softphone’s read-back stabilises on the last write.
Example of the persisted shape:
Read your current presence
Two GET paths complement the PATCH pair:GET /api/v1/users/me/presence/schedule— returns your persisted recurring envelope (data.dnd_schedule) ornullwhen you have never configured one. Use it to prefill a schedule editor or to confirm{"enabled": false}actually landed.GET /api/v1/users/presence— an organization-scoped roster of every active user with their current presence. Your own row appears with the same fields as the PATCH echo, plus a best-effort DND-expiry projection: if yourdnd_untilis in the past, the roster surfaces you asavailableimmediately, without waiting for the cleanup worker’s next tick. The roster is capped at 500 rows with atruncatedflag. Reading the roster is how a teammate dashboard or a routing-rules UI answers “who is actually reachable right now” without polling the PATCH echo.
Federation precedence: manual vs external sources
Federation sources (Microsoft Teams via Microsoft 365, Slack, Cisco Webex, Zoom, Google Calendar, Microsoft 365 Calendar) can write the same presence record your manual settings write. The merge is resolved once per 60-second sync tick under a deterministic rule — the same rule independent of which source arrived first: Most-busy-wins. The effective presence is the least-available value across every active source and your native setting, prioritiseddnd > busy > away > available. Two consequences matter:
- Federation can only make you busier, never freer. If you set yourself to
dndorbusymanually, an external “Available” never drags you back; a federated “DoNotDisturb” from Microsoft Graph or a Slack DND snooze always survives the merge. Manual settings behave like one more federation input at native priority — the native signal never drops in priority, and a racing manual PATCH wins over the scheduler’s single merged write by last-write-wins. - Federation never manages
dnd_untilor the schedule. The timed-DND expiry and the recurring schedule below are owned by the manual PATCH path. Federation updatespresence_statusandpresence_set_at; it leaves the window contract alone, so an external flip can never erase a manual timed DND or a business-hours envelope.
GET /api/v1/users/me/presence/federation returns every source row including disabled ones, so diagnostics survive a toggle-off.
When the API should take precedence: take manual control any time you want busier-than-federated routing — a “focus now” DND, a PTO busy, an in-person event the provider can’t see, or a deliberate override of stale/unreached sources. The external markers hold no veto over your manual
dnd_until because the sync does not write that field.
Recurring DND schedule (business hours)
Set the envelope once; a 60-second worker tick flips presence betweenavailable inside the window and dnd outside it:
A manual time-bounded
dnd_until always wins over the schedule — a timed DND you set directly stays in force through its own expiry, then the schedule resumes.
Disable the automation without touching presence:
enabled, timezone, dedup-sorted daysOfWeek, startMinute, endMinute). The 60-second flip is monotone in each direction: it writes available inside the window, dnd outside, and never touches a user whose active manual timed DND is currently sourced outside the schedule (the manual override always wins — the scheduler skips those rows by design).
Observability: realtime events and cache propagation
An accepted write (manual PATCH, /schedule PATCH, or a federation sync tick) fans out three ways, and understanding them is the difference between “presence stuck” and “presence propagating”: -user.presence.changed realtime event — published on the tenant channel events:<tenant_schema> the moment the row is accepted. Any teammate dashboard subscribed on GET /events/stream sees the flip in under 2 seconds without polling; the event payload mirrors the PATCH echo (user_id, presence_status, presence_set_at, dnd_until, status_message, presence_activity). A failed publish never fails the PATCH — the database row remains the source of truth and late subscribers catch up on the next GET /users/presence roster load.
- Inbound DND routing cache — each user’s DND lookup rides a 30-second Redis cache (key pattern
dnd:user:<userId>) so the voice path doesn’t slam the database per call. The PATCH handler deletes that key synchronously before returning, which makes the user-visible rule hold: DND OFF → next call rings; DND ON → next call falls to voicemail. When the invalidation itself would fail (Redis impairment), the 30-second TTL is the worst-case bound — never longer; there is no path where a confirmed PATCH leaves routing stale beyond one TTL window. - Federation merge runs at its own cadence — the scheduler’s 60-second tick means an external flip (Slack DND beginning, Graph reporting a meeting) propagates up to one tick after the provider reports it. The manual PATCH path, with the synchronous event + cache invalidation above, always lands first.
GET /events/stream on the integration consuming presence (your own dashboard, an attendant console, a routing analytics pipeline), filter for type = "user.presence.changed", and check dnd_until + status_message on each event — a silent subscription means routing is still correct because the DTN cache was invalidated even when the event stream was missed.
Troubleshooting
Work from the outward symptom to the writer. The same knob — manual PATCH, schedule, a federation source, or the GET rosters — belongs to exactly one stage below.An agent is skipped and the call goes to voicemail
- Check your own record in the team roster (
GET /api/v1/users/presence): if it showsdnd, routing is working as designed — one of the writers below set DND. If it showsavailablebut calls still skip, the problem is in the route (fallback destination), not presence. - A federation source flipped you. Teams in a meeting, Slack snooze, Zoom presence, or a calendar busy block fold into the merge; check the federation rows (
GET /api/v1/users/me/presence/federation) forlast_statusandlast_synced_at. Disabling the source removes it from the next merge tick. - The recurring schedule thinks it is outside hours. Read
GET /api/v1/users/me/presence/schedule— an enabled envelope with the wrong timezone or an unexpected day set flips you on every 60-second tick. Send{"enabled": false}to take it out of the merge. - A leftover manual
dnd_until. The most common cause of a “ghost” DND: send a non-DND status ({"status":"available"}) — it clears the window in the same call, and the routing cache invalidation fires inline.
DND never clears
- You went indefinite. A DND set without a
dnd_untilwaits for any non-DND status to land — there is no clock that ends it. PATCH an explicitdnd_until(or flip toavailable) to put an expiry on it. - Your expiry is in the past. The PATCH rejects a past
dnd_untilwithVALIDATION_ERROR, so a wrongly-dateddnd_untilnever lands — check the caller actually got the 400 instead of assuming the DND is timed. - The roster says DND but looks stale. The GET roster projects expired
dnd_untilrows asavailableon read; if it still showsdndyour window genuinely has not expired yet — check the timestamp, not the clock.
Manual dnd_until vs the recurring schedule
The manual window wins until it expires; the schedule resumes the moment dnd_until lapses. The flip-side surprise is the reverse order: when the schedule and a manual DND overlap, the schedule pauses and re-arms after your manual window — you are never forcibly re-flipped mid-override. If the schedule unexpectedly re-checked you back in, verify GET /users/me/presence/schedule: the envelope returned is the canonical one (sorted days, end-of-day 1440 sentinel preserved) — the schedule tick owns the flip for enabled envelopes.
Federation is fighting your manual status
If something keeps re-flipping you busier, that is by design — the merge is most-busy-wins and federation never makes you freer. If it keeps flipping you busier than any source’s raw value (e.g.dnd while Slack reports only away and the calendar is empty), the pathway is a stale debounce: the source row still holds a DND mapped state from an earlier sync. Disable the source with {"enabled": false}, flip yourself available, and the scheduler stops reading it on its next tick. Re-enable after any provider reconnect; the next success tick updates last_synced_at/last_status and the merge re-forms cleanly.
What presence does to inbound routing
dnd (or an in-force DND window) removes you from eligible claims the moment it lands — the routing cache invalidation is synchronous, not eventual. busy blocks the voice claim while the digital-assignment ledger still counts your load normally. For the ACD-audit semantics behind these four public values (and the five-state model dispatch actually checks), see Agent presence and aux-code lifecycle.
Related
- Presence federation settings — let Teams, Slack, Webex, Zoom calendars write this same state.
- Agent presence and aux-code lifecycle — the ACD state machine behind the four public values.
- My voice preferences — the sibling “Me” settings endpoints.