Developer Portal
Experimental The bot & app platform is in active development. Self-hosted servers run bots, slash commands and message components with a few differences from cloud. See what's supported.
← Docs

Rich Presence

An activity is what someone is doing right now: Playing Tibia, with Hunting in Roshamuul, Level 412 Elite Knight, an image and a timer underneath. GameVox uses Discord’s activity object and Discord’s ways of setting it, so code written for Discord Rich Presence mostly works unchanged. GameVox decides which name is shown and screens everything else.

Overview

There are four ways in, and one way out:

  • Your bot’s own status: the Identify presence and gateway op 3, exactly as on Discord.
  • A user’s activity, set from your server: a REST call with your bot token, for users who allowed your app (the activities.write scope). Your app needs GameVox’s approval for this.
  • A user’s activity, set by a program on their computer: the desktop app’s local RPC, which speaks the Discord RPC IPC protocol.
  • GameVox’s own game detection, which you do not control but which your activity can merge with.
  • Out: bots with the GUILD_PRESENCES intent receive PRESENCE_UPDATE when a member’s status or activity changes.

Where it shows: under the name in a server’s member list, on the profile card (image, details, state, party, timer, buttons), in the DM header and in the friends list, on desktop, web, iOS and Android.

What is shown

A person (or bot) shows at most one Custom status (type 4) and one other activity. When several sources say something at once, the local RPC wins over an app, an app over a bot’s own activity, and any of them over the detected game. An activity for the same game GameVox detected is merged with it: your details, state and images, GameVox’s game name and start time when you sent none. An activity for something else replaces the detected game. People cannot set a Custom status yet; bots can.

The activity object

The fields are Discord’s. What you may send depends on who sends it (see each section). Everything is cleaned: control characters and direction overrides are removed, line breaks become spaces, runs of whitespace collapse, and text is cut to the limits below.

FieldNotes
name Up to 128 characters. Bots choose their own name. For a user’s activity GameVox chooses the name and ignores yours (see “Names”).
type See the table of types. Which types are allowed depends on the source.
url Streaming only, and only a twitch.tv or youtube.com https link.
timestamps Unix time in milliseconds or seconds (values below 1011 are read as seconds). start counts up, end counts down. Refused if before 2000, if start is more than a day ahead, or if end is before start.
details, state Up to 128 characters each. The first and second line of the card.
status_display_type 1 shows the state instead of the name in the one-line form (“Playing Level 412 Elite Knight”), 2 the details. 0 or absent shows the name.
emoji Custom status only: a Unicode emoji (the name, up to 32 characters). Custom emoji ids are dropped.
party id up to 128 characters, size [current, max] with 0 ≤ current ≤ max ≤ 10000. Shown as “2 of 5”. A bad size drops only the size.
assets Keys of images you uploaded to your application on the Rich Presence tab (1 to 32 of a-z 0-9 _ -), and hover texts of up to 128 characters. URLs and mp: values are dropped. See “Images”.
buttons Up to two, each {label, url}: label up to 32 characters, https URL up to 512. Only for approved and first-party apps. Members open them through GameVox’s external-link warning; nobody sees buttons on their own card.

The whole activity, once cleaned, must fit in 4096 bytes of JSON. Links (url, button URLs, details_url, state_url) must be https, on a public host name, with no user name, IP address or port. application_id, flags, instance, secrets, sync_id and anything else you send are replaced or dropped.

Activity types

TypeShows as
0 Playing {name}
1 Streaming {details or name}
2 Listening to {name}
3 Watching {name}
4 {emoji} {state}
5 Competing in {name}

The one-line form is translated into the viewer’s language by GameVox; your text is not.

Your bot’s status

Set it the Discord way: the presence field of Identify (op 2), and gateway op 3 (Presence Update) whenever it changes. discord.js client.user.setPresence(), discord.py change_presence() and the like work as they are.

{
  "op": 2,
  "d": {
    "token": "<bot token>",
    "intents": 513,
    "properties": { "os": "linux", "browser": "my-bot", "device": "my-bot" },
    "presence": {
      "since": null,
      "activities": [ { "name": "12 Tibia worlds", "type": 3 } ],
      "status": "online",
      "afk": false
    }
  }
}
{
  "op": 3,
  "d": {
    "since": null,
    "activities": [ { "name": "Custom Status", "type": 4, "state": "/help for commands" } ],
    "status": "dnd",
    "afk": false
  }
}

// discord.js
client.user.setPresence({
  activities: [{ name: 'raid signups', type: ActivityType.Watching }],
  status: 'idle',
});

# discord.py
await client.change_presence(activity=discord.Game(name="VALORANT"), status=discord.Status.online)

What GameVox uses

  • status: online, idle, dnd, invisible or offline. Idle and Do Not Disturb change the bot’s dot. Invisible and offline show the bot as offline with no activity, while the bot stays connected and keeps working. A missing status means online; any other value and the whole update is ignored.
  • activities: per activity only name, type, state, url (Streaming) and the emoji name (Custom). Everything else is dropped. The list is reduced to one Custom status plus one other activity.
  • A Custom status (type 4) needs a state or an emoji; its name may be anything (Discord sends “Custom Status”).
  • since and afk are accepted and ignored.
  • An Identify without presence resets the bot to plain online. A Resume keeps the status the bot had.

Limits

  • Op 3 is limited to 5 per 20 seconds per connection. Over the limit, GameVox keeps the newest update and applies it when the window allows. The connection is never closed for it.
  • Members whose client is open see a change within 30 seconds: GameVox sends at most one update per bot every 30 seconds. A member list opened later shows the current status.
  • Sending the same presence again changes nothing.

The status shows under the bot’s name in the member list and on its profile, to the members of every server the bot is in, including self-hosted ones. Other bots do not receive it.

Setting a user’s activity from your app

Your server can show what a user is doing in your game, for example from your game server’s own data, without anything running on the user’s computer. This is a GameVox extension: Discord has no such endpoint.

What it takes

  1. Approval. GameVox approves apps for this by hand. Until yours is approved, every PUT answers 403 with code 50001. Ask GameVox support. GameVox’s own first-party apps are approved already.
  2. The user’s consent. The user authorizes your app with the activities.write scope through the normal OAuth2 consent screen (Log in with GameVox). Users can revoke it at any time in their settings; their activity from your app disappears at once.
  3. The user’s switch. A user who turned off “Let apps set my activity” gets nothing shown. Your PUT still answers 204, so you cannot tell.
https://gamevox.com/oauth2/authorize/?client_id=1491200000000000000
    &response_type=code
    &scope=identify%20activities.write
    &redirect_uri=https%3A%2F%2Fexample.com%2Fgamevox%2Fcallback
    &state=8f2c1a

Set and clear

Authenticate with your bot token. {user_id} is the user’s snowflake, as your bot sees it everywhere else.

PUT https://app-api.gamevox.com/api/v10/applications/@me/users/1491250000000000003/activity
Authorization: Bot <token>
Content-Type: application/json

{
  "activity": {
    "type": 0,
    "details": "Hunting in Roshamuul",
    "state": "Level 412 Elite Knight",
    "timestamps": { "start": 1761868000000 },
    "party": { "size": [2, 5] },
    "assets": {
      "large_image": "roshamuul", "large_text": "Roshamuul",
      "small_image": "knight", "small_text": "Elite Knight"
    },
    "buttons": [ { "label": "View character", "url": "https://example.com/characters/412" } ]
  },
  "ttl_seconds": 300
}

HTTP/1.1 204 No Content

DELETE https://app-api.gamevox.com/api/v10/applications/@me/users/1491250000000000003/activity
Authorization: Bot <token>

HTTP/1.1 204 No Content

How GameVox reads it

  • ttl_seconds: how long the activity lives without a refresh, 60 to 900 seconds (default 300; values outside are clamped). PUT the current state again before it runs out. If your service stops, the activity disappears by itself.
  • name is ignored. GameVox shows the name of the game your app is linked to, or else your app’s name, and always adds “via” your app.
  • Types 0, 1, 2, 3 and 5. Custom status (4) is refused.
  • A PUT replaces your previous activity for that user. A refresh that changes nothing only extends the TTL. created_at stays the same while type and name do, so the card does not look new.
  • Image keys must be assets of your application. Unknown keys are dropped and the text still shows.
  • DELETE removes only your app’s activity for that user and needs no grant. It answers 204 whether or not there was one.

Errors

HTTPCodeNotes
204 Success. Also the answer when the user turned off “Let apps set my activity” (nothing is stored).
400 50035 The body is not JSON, activity is missing or not an object, ttl_seconds is not a whole number, or the activity was refused. errors.activity.<field>._errors[0].code says why: ACTIVITY_TYPE_INVALID, ACTIVITY_NAME_REQUIRED, ACTIVITY_TIMESTAMPS_INVALID, ACTIVITY_TOO_LARGE, ACTIVITY_INVALID, BASE_TYPE_REQUIRED, NUMBER_TYPE_COERCE.
403 50001 Your app is not approved for user activity, or the user has not granted activities.write to it (or revoked it).
404 10002 Unknown application.
404 10013 Unknown user (the snowflake is not a GameVox user).
413 The body is larger than 16 KB.
429 Rate limited. The body is Discord’s, with retry_after.
HTTP/1.1 400 Bad Request

{
  "code": 50035,
  "message": "Invalid Form Body",
  "errors": {
    "activity": {
      "type": {
        "_errors": [
          { "code": "ACTIVITY_TYPE_INVALID",
            "message": "This activity type is not allowed for an application. Use 0, 1, 2, 3 or 5." }
        ]
      }
    }
  }
}

Limits

  • 5 requests per 20 seconds per user, PUT and DELETE counted separately.
  • 50 requests per second per application across all users.

Images

Upload images on your application’s Rich Presence tab, each with a key (1 to 32 characters, letters, digits, _ and -, unique per app, case does not matter). PNG, JPEG or WebP up to 1 MB; every upload is checked before it is used. In an activity, put the key in assets.large_image or assets.small_image.

  • GameVox looks the key up among the assets of the application the activity belongs to: your app for the REST endpoint and for a bot, the app the local client id stands for on RPC. Nothing else can be referenced.
  • Without a large image GameVox shows its own art for the game, then your app’s icon. The small image only shows on top of a large one.
  • A deleted asset disappears from cards within a few minutes.
  • Image URLs, Discord mp: media proxy values and keys of other apps are dropped.

Listing an app’s assets

The asset list is public, like Discord’s, so tools can check which keys exist. No authentication.

GET https://app-api.gamevox.com/api/v10/oauth2/applications/1491200000000000000/assets

HTTP/1.1 200 OK
Cache-Control: public, max-age=300

[
  { "id": "6f1c2e3a-…", "name": "roshamuul", "type": 1 },
  { "id": "9b0d4c7e-…", "name": "knight", "type": 1 }
]
  • Image assets only, oldest first. id is the asset’s id, name its key, type always 1.
  • An unknown application answers 404 with code 10002.
  • Answers are cached for 5 minutes, so a new upload can take that long to show. 30 requests per minute per IP.

Desktop local RPC

A program on the user’s computer (a game, a launcher, a media player) can set the user’s activity through the GameVox desktop app (Windows, macOS, Linux). It speaks the Discord RPC IPC protocol, so the Discord Game SDK’s and the community Rich Presence libraries’ framing work as they are. The web, iOS and Android apps have no local RPC.

Where to connect

  • Windows: the named pipe \\.\pipe\gamevox-ipc-0 (try -0 to -9).
  • macOS and Linux: the Unix socket gamevox-ipc-0 to -9 in $XDG_RUNTIME_DIR, or else in $TMPDIR/gamevox-<uid>/.
  • Only programs running as the same OS user can connect. Nothing listens on the network, and there is no WebSocket or HTTP transport.
  • Users can turn the local RPC off in GameVox’s settings (it is on by default).

Compatibility mode for programs built for Discord RPC

A program that only knows the Discord pipe names finds GameVox if the user turns on compatibility mode in GameVox’s settings (off by default). GameVox then also answers on discord-ipc-0 to -9, but only while Discord is not running: it steps aside as soon as Discord starts and checks again every 10 seconds. If you can, connect to gamevox-ipc-N yourself; most libraries let you set the pipe name, and then compatibility mode is not needed.

Framing

Each frame is an 8-byte header, the opcode and the payload length as unsigned 32-bit little-endian integers, followed by that many bytes of UTF-8 JSON. Frames larger than 64 KB close the connection.

OpcodeNotes
0 HANDSHAKE The first frame, and only the first.
1 FRAME A command and its reply.
2 CLOSE Either side closes. GameVox sends {code, message} when it refuses a connection.
3 PING GameVox answers with PONG and the same payload.
4 PONG Answer to PING.

Handshake and READY

Send {"v": 1, "client_id": "…"} within 10 seconds of connecting. client_id is your application id (15 to 21 digits). GameVox answers with a READY dispatch carrying the signed-in user (their GameVox snowflake). A bad handshake is answered with CLOSE and code 4000.

→ op 0 HANDSHAKE
{ "v": 1, "client_id": "1491200000000000000" }

← op 1 FRAME
{
  "cmd": "DISPATCH",
  "evt": "READY",
  "nonce": null,
  "data": {
    "v": 1,
    "config": {
      "cdn_host": "files.gamevox.com",
      "api_endpoint": "//app-api.gamevox.com/api",
      "environment": "production"
    },
    "user": {
      "id": "1491250000000000003", "username": "ben", "discriminator": "0",
      "global_name": "Ben", "avatar": null, "bot": false, "flags": 0, "premium_type": 0
    }
  }
}

SET_ACTIVITY

The reply carries the activity as GameVox accepted it. "activity": null clears it, and so does closing the connection. The latest program to set an activity owns the one local activity.

→ op 1 FRAME
{
  "cmd": "SET_ACTIVITY",
  "nonce": "7c1f0a52",
  "args": {
    "pid": 4242,
    "activity": {
      "type": 0,
      "details": "Hunting in Roshamuul",
      "state": "Level 412 Elite Knight",
      "timestamps": { "start": 1761868000 },
      "assets": { "large_image": "roshamuul", "large_text": "Roshamuul" },
      "party": { "id": "p1", "size": [2, 5] }
    }
  }
}

← op 1 FRAME
{
  "cmd": "SET_ACTIVITY",
  "evt": null,
  "nonce": "7c1f0a52",
  "data": {
    "id": "a41c09e2f3b8d715", "name": "Tibia", "type": 0, "url": null,
    "created_at": 1761868800000,
    "timestamps": { "start": 1761868000000 },
    "application_id": "1491200000000000000",
    "details": "Hunting in Roshamuul", "state": "Level 412 Elite Knight", "emoji": null,
    "party": { "id": "p1", "size": [2, 5] },
    "assets": { "large_image": "roshamuul", "large_text": "Roshamuul" },
    "flags": 0
  }
}
CommandNotes
SET_ACTIVITY Sets or clears the activity (see above). Types 0, 2, 3 and 5 only; anything GameVox refuses is answered with evt: "ERROR" and code 4000.
SUBSCRIBE Acknowledged so SDKs that subscribe to join, spectate or ready events keep working. GameVox never dispatches those events.
UNSUBSCRIBE Same as SUBSCRIBE.
* Anything else (AUTHORIZE, AUTHENTICATE, GET_GUILDS, voice and lobby commands…) is answered with evt: "ERROR", code 4006, “Not supported by GameVox”.

Consent and identity

  • Nothing leaves the computer until the user allows your program. The first time, GameVox asks: Allow, Not now or Never. The answer is remembered per executable and client id. Meanwhile SET_ACTIVITY replies as usual and the activity waits.
  • GameVox allows without asking when your program is the game it detected and your client id is linked to that game on GameVox.
  • GameVox learns who you are from the operating system (the process at the other end of the pipe and its executable), never from args.pid or anything else in the payload.
  • The name is GameVox’s choice, not yours: the game or app your client id is linked to on GameVox, else the game GameVox detected if your executable is that game, else the game its catalog knows your executable as, else “an app”. name in your activity is ignored.
  • Buttons are shown only when your client id belongs to an approved or first-party GameVox app. Images resolve against that app’s assets.
  • Text is checked again by GameVox’s servers, and unacceptable details or state are removed.

Limits: 5 SET_ACTIVITY per 20 seconds per connection, at least 500 ms apart; faster updates are merged and the latest wins. At most 16 programs connected at once.

Receiving members’ presence

With the GUILD_PRESENCES intent your bot receives PRESENCE_UPDATE when a member of a server it is in changes status or activity, in Discord’s shape.

{
  "op": 0,
  "t": "PRESENCE_UPDATE",
  "d": {
    "user": { "id": "1491250000000000003" },
    "guild_id": "1491230000000000001",
    "status": "online",
    "activities": [ {
      "id": "a41c09e2f3b8d715", "name": "Tibia", "type": 0, "url": null,
      "created_at": 1761868800000,
      "timestamps": { "start": 1761868000000 },
      "application_id": "1491200000000000000",
      "details": "Hunting in Roshamuul", "state": "Level 412 Elite Knight", "emoji": null,
      "assets": { "large_image": "roshamuul", "large_text": "Roshamuul" },
      "buttons": [ "View character" ],
      "flags": 0
    } ],
    "client_status": { "desktop": "online" }
  }
}
  • status is online, idle, dnd or offline. An invisible member is offline with no activities. client_status always names desktop.
  • You see what the server’s members would see: no game a member does not share with this server, no adult game they have not chosen to show, and only name and type from a member who turned off details.
  • Activities are Discord’s object with an id and created_at. buttons are labels only; you never get a button URL, and none of GameVox’s own fields.
  • Changes only: there are no presences in GUILD_CREATE or in member chunks. Changes are grouped per member over about 2 seconds.
  • Bots’ own statuses are not sent to other bots.

Self-hosted servers

On a self-hosted server:

  • Your bot’s status works the same and shows to the server’s members.
  • The desktop local RPC works the same, but the server cannot look up client ids: it shows the detected game’s name when your activity is for that game, else “an app”. No buttons, and images only from GameVox’s own file host.
  • Activities set from your app with the REST endpoint show in GameVox’s cloud (profiles, friends, cloud servers), not on self-hosted servers.
  • PRESENCE_UPDATE works the same, sent by the server itself.

Not supported

  • Join, spectate and ask-to-join (secrets, party.privacy, lobbies), and the RPC events that go with them.
  • Image URLs and Discord media proxy (mp:) images: upload assets instead.
  • The RPC over WebSocket or HTTP, and RPC authorization (AUTHORIZE / AUTHENTICATE).
  • Custom status for people, and headless sessions.

Gateway intents →  ·  Back to docs