Presence (Status)
A user's presence is four things: whether they can be reached (online),
whether somebody is looking at a client of theirs (active), what they say
about being reachable (state), and a custom status of their own — an emoji
with a caption.
Get Contact Statuses
Request (all contacts):
{"id": "40", "type": "presence.get"}
Request (specific user):
{
"id": "40",
"type": "presence.get",
"data": {
"user_id": 67890
}
}
Success:
{
"id": "40",
"type": "presence.get",
"ok": true,
"data": {
"statuses": [
{"user_id": 67890, "online": true, "active": true, "state": "away", "custom": {"emoji": "🍕", "text": "Lunch", "until": "2024-01-15T15:00:00Z"}, "time": "2024-01-15T14:00:00Z"},
{"user_id": 67891, "online": true, "active": false, "last_active_at": "2024-01-15T12:30:00Z", "state": "available", "time": "2024-01-15T09:00:00Z"},
{"user_id": 67892, "online": false, "active": false, "last_active_at": "2024-01-15T10:00:00Z", "custom": {"emoji": "🏖️", "text": "On vacation"}, "time": "2024-01-15T10:00:00Z"}
]
}
}
| Field | Type | Description |
|---|---|---|
user_id |
number | The user's ID |
online |
boolean | Whether the user can be seen online |
active |
boolean | Whether somebody is looking at a client of the user |
last_active_at |
string | While nobody is, when somebody last was (RFC 3339); absent when that was never known |
state |
string | What the user says about being reachable; only while online |
custom |
object | The user's custom status; absent when there is none |
time |
string | When the user last went online or offline (RFC 3339) |
State field: what the user says about being reachable — available,
away, busy or invisible. It is kept with the account until the user
changes it: going offline and coming back does not reset it, so a phone that
falls asleep does not turn busy into available. It is reported only while
the user is online, because an account that is gone is not away. A new account
starts available.
invisible keeps the user online for themselves and offline for everybody
else: contacts receive presence.offline when the user turns invisible, see
them offline with the moment they did, and receive presence.online when they
come back. Messages, pushes and everything else work as usual, except what
would give the user away: a delivery they
report is not recorded, and how far they
read a direct conversation reaches the other
side only when they are visible again, after presence.online — which tells
it the messages read were delivered as well. Only the user's own devices are
ever told invisible.
Custom field: an emoji with an optional caption that the user shows next
to their name, such as 🍕 "Lunch" or 🏖️ "On vacation". Unlike the state it is
shown whether the user is online or not. until, when present, is the moment
it clears itself; once it has passed, it is no longer shown, and within a
minute the server clears it and tells the user's devices and contacts.
| Field | Type | Description |
|---|---|---|
emoji |
string | One emoji |
text |
string | Caption; absent when there is none |
until |
string | When the status clears itself (RFC 3339); absent to keep it until changed |
A user is online while they can be reached: at least one of their connections
is open, active or not, or at least one of their
sessions is registered for pushes — unless they are
invisible. time is when that last changed.
A user is active while somebody is looking at a client of theirs: one of
their connections is active and they are not
invisible. A page left in a background tab, or a phone a push keeps online, is
online without being active, which is what a messenger shows as "last seen":
last_active_at. Becoming active reaches contacts at once; nobody looking any
more only after a grace of about half a minute without anybody looking again,
so that a switch between apps tells them nothing, and then as
presence.updated with the moment it began. A user who goes
offline or invisible stops being active with it, and while invisible their
last_active_at stays where it was when they became so. A transport adapter
whose protocol has no word for it, OSCAR's, leaves active out.
Results include only users allowed by their current presence setting, which
governs the custom status as much as the rest. A forbidden specific lookup
returns user_not_found; an all-contacts lookup omits forbidden users. This
avoids revealing whether a hidden user is online.
Asking about oneself is how a device learns what the account says: the answer
carries the state whether or not the account is online, invisible included,
and online says whether the account can be reached at all.
Set Presence
{"id":"41","type":"presence.set","data":{"state":"away"}}
{"id":"42","type":"presence.set","data":{"custom":{"emoji":"🍕","text":"Lunch","until":"2024-01-15T15:00:00Z"}}}
{"id":"43","type":"presence.set","data":{"custom":null}}
| Field | Type | Required | Description |
|---|---|---|---|
state |
string | No | available, away, busy or invisible |
custom |
object | No | A new custom status, or null to clear it |
A request carries either field or both; a field left out stays as it is. The state and the custom status are independent: one may be away with a custom status of 🍕 "Lunch", or available with none.
Both apply to the whole account rather than to one device, so a client that
sets them changes what every other one shows. They are kept until changed,
whatever connections come and go; the state shows while the user is online, a
custom status at once, online or not.
The response is the caller's own status as asking about
oneself gives it, and setting what is already set
changes nothing and tells nobody. The account's other devices receive the new
status as presence.self.
Presence Events (Push)
Server notifies when contacts come online, go offline, or change what they say
about themselves. Every event carries the same object presence.get lists, as
the receiver may see it.
User Online:
{
"type": "presence.online",
"data": {
"user_id": 67890,
"online": true,
"active": true,
"state": "available",
"time": "2024-01-15T14:00:00Z"
}
}
User Offline:
{
"type": "presence.offline",
"data": {
"user_id": 67890,
"online": false,
"active": false,
"last_active_at": "2024-01-15T14:30:00Z",
"custom": {"emoji": "🏖️", "text": "On vacation"},
"time": "2024-01-15T14:30:00Z"
}
}
Presence Changed:
{
"type": "presence.updated",
"data": {
"user_id": 67890,
"online": true,
"active": false,
"last_active_at": "2024-01-15T14:09:30Z",
"state": "away",
"custom": {"emoji": "🍕", "text": "Lunch"},
"time": "2024-01-15T14:10:00Z"
}
}
presence.updated reports a new state of somebody online, somebody online
becoming active or no longer, and a new custom status of anybody, online or
not — set, cleared, or run out. A user turning invisible or visible again
reaches contacts as presence.offline or presence.online.
These events are about contacts: they reach whoever lists the user, the user included if they listed themselves, with what contacts may see. What the account says about itself reaches its own devices separately:
Own Status Changed:
{
"type": "presence.self",
"data": {
"user_id": 12345,
"online": true,
"active": true,
"state": "invisible",
"custom": {"emoji": "🍕", "text": "Lunch"},
"time": "2024-01-15T14:10:00Z"
}
}
presence.self carries the account's own status as asking about
oneself gives it. It reaches the account's other
devices when one of them sets the state or the custom status, and all of them
when the custom status runs out.
A presence.offline sent because the receiver is no longer allowed to see the
user carries nothing but user_id, online and an empty time.
Contact Requests
Somebody who writes to a user who does not have them as a contact — a direct message, a forward or a direct transfer — asks to be taken in. The user is told at once, before the message itself, by an event of their account stream:
{
"type": "contact.requested",
"seq": "45",
"data": {
"contact": {
"user_id": 67890,
"nick": "Alice",
"avatar_hash": "a1b2c3d4",
"requested_at": "2024-01-15T14:00:00Z"
}
}
}
The entry is the one contact events carry: nick is the
name the person goes by (Add Contact), a snapshot, and
requested_at when they asked.
A request is not a contact. No privacy setting that trusts
contacts trusts it, and contact.list returns it apart from the contacts, in
requests, newest first. The user takes the person in with
contact.add, which makes them a contact and answers the
request, or turns them away with contact.delete, which
counts as taking them out of the list: nothing asks about them again, and no
address book brings them back. A client answering a request it shows passes
"request": true, so that an answer given after the request was settled
elsewhere changes nothing. Either reaches the user's other clients as
contact.added or contact.deleted. An address book that
finds the person takes them in too, which answers the request: the uploading
session learns it from matches[].added, and every session of the user from
contact.added. Replying to a request does not answer it.
Nobody is asked about who is a contact already, somebody turned away or taken
out of the list, or somebody a block separates from the user. Somebody who
answers a direct chat the user started — whose first message is the user's —
is not asked about either: the user chose to write to them, so they are taken
in as a contact, and the user's clients are told with contact.added before
the message:
{
"type": "contact.added",
"seq": "46",
"data": {
"contact": {"user_id": 67890, "nick": "Alice", "avatar_hash": "a1b2c3d4"},
"presence": {"user_id": 67890, "online": true, "state": "available", "time": "2024-01-15T14:00:00Z"}
}
}
A request's typing reaches the user as a contact's does. A block ends the requests between the two either way.
A bot is written to as a service is used: it asks nobody to be taken in, nobody asks it, and writing to one takes it into no contact list. Its typing reaches whoever it talks with all the same: it is in no direct chat but one they began.