Veyra API documentation
Veyra is a Discord presence and profile API for developers who want supported data for users tracked by the Veyra service. It exposes a REST snapshot endpoint, a WebSocket stream, a status JSON endpoint, and a Discord bot command for viewing registered information.
Tracked users only. Having a Discord ID is not the same as being registered with Veyra. The API returns USER_NOT_TRACKED when the user is not stored in Veyra.
REST snapshot
GET /api/user/:userId returns the tracked user's profile, presence, activities, Spotify state, guild membership data, badges, and timestamps.
WebSocket stream
/socket sends INIT_STATE, PRESENCE_UPDATE, and USER_REMOVED events for subscribed user IDs.
Status endpoint
GET /health/json reports service health, Discord readiness, database state, tracked user count, WebSocket connections, uptime, and timestamp.
How Veyra works
The implementation tracks non-bot members of the configured Veyra guild. The source defines the guild as 1399305230268760186. On startup, Veyra fetches guild members, adds or updates current members, and removes stored users who are no longer members.
Join the official Veyra Discord
Users join https://discord.gg/ZTeU5HxznV.
Complete server verification
The project prompt identifies server verification as required. The provided source code does not include a verification-channel handler, so verification appears to be handled by Discord server configuration or another system outside this repository.
Veyra stores the user
On startup sync or guildMemberAdd, the bot waits 5 seconds, confirms the user is still in the guild, and upserts their profile and current presence into MongoDB.
Presence and profile updates continue
presenceUpdate, userUpdate, and guildMemberUpdate update stored data and broadcast WebSocket updates to subscribers.
Leaving removes tracking
guildMemberRemove deletes the user document and sends a USER_REMOVED WebSocket event to subscribed clients.
Architecture
About Veyra
Veyra is built and maintained by Lorb (@6lxrb). The official creator website is https://lorbspace.vercel.app.
Official links: Veyra Documentation, Veyra Discord, and Lorb.
Quick start
- Join the official Veyra Discord at discord.gg/ZTeU5HxznV.
- Complete the server's required verification.
- Wait for Veyra to register and track your guild membership. The implementation waits 5 seconds after joining before inserting a new user.
- Run
/infoin the Veyra Discord to view your API URL. - Make a request to
GET /api/user/:userId.
curl https://veyra-db3d.onbelmo.uk/api/user/YOUR_DISCORD_USER_IDconst response = await fetch("https://veyra-db3d.onbelmo.uk/api/user/YOUR_DISCORD_USER_ID");
const body = await response.json();
if (!body.success) {
throw new Error(`${body.error.code}: ${body.error.message}`);
}
console.log(body.data.discord_status);
console.log(body.data.discord_user.global_name || body.data.discord_user.username);type VeyraResponse<T> =
| { success: true; data: T }
| { success: false; error: { code: string; message: string } };
const res = await fetch("https://veyra-db3d.onbelmo.uk/api/user/YOUR_DISCORD_USER_ID");
const body = (await res.json()) as VeyraResponse<{ discord_status: string }>;
if (!body.success) throw new Error(body.error.message);
console.log(body.data.discord_status);import requests
res = requests.get("https://veyra-db3d.onbelmo.uk/api/user/YOUR_DISCORD_USER_ID", timeout=10)
body = res.json()
if not body["success"]:
raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]}')
print(body["data"]["discord_status"])Become a tracked user
Veyra can only return supported data for users registered in its database. Registration is created from guild membership, not from arbitrary Discord IDs.
| Case | Actual behavior from source |
|---|---|
| Valid Discord ID, not tracked | GET /api/user/:userId returns 404 USER_NOT_TRACKED. |
| User joins the Veyra guild | The guildMemberAdd handler waits 5 seconds, confirms membership, then upserts the user. |
| Server startup | syncGuildMembers fetches all guild members, upserts non-bot members, and deletes stale records. |
| User leaves | guildMemberRemove deletes the user's database record and broadcasts USER_REMOVED. |
| User rejoins | The user is upserted again after the 5 second join delay if still present. |
| Verification fails | No verification-failure path exists in the provided source. If the server removes or blocks the user outside this app, Veyra will not keep a tracked record. |
/info command
The Veyra Discord bot registers a guild slash command named /info. It can show information for yourself or another selected user.
The command only works in the Veyra guild. If the command is used outside the guild, the bot replies ephemerally: Veyra only works in the Veyra guild.
| Displayed item | Source | Notes |
|---|---|---|
| Monitored Since | tracked_since | Formatted as a Discord timestamp and relative time. |
| Account Creation | Discord snowflake timestamp | Calculated locally from the user ID. |
| Badges | discord_user.badges | Displays comma-separated badge names or None. |
| Veyra API URL | BASE_URL + /api/user/:userId | Also appears as a link button labeled API. |
| K/V Keys | Hard-coded command field | Currently displays none. |
| Note (kv.note) | Hard-coded command field | Currently displays none. |
Personal API URL. The source builds this URL from the public base URL and Discord user ID. No expiration, regeneration, authentication, or private token behavior is implemented. Treat it as a public link to public Veyra-tracked data.
Authentication
The public REST API and WebSocket gateway do not require API keys or bearer tokens in the provided implementation. CORS allows any origin for GET and OPTIONS.
The server allows an Authorization header through CORS, but no public route validates it. The internal badge fetcher may use DISCORD_USER_TOKEN server-side; clients never send that token to Veyra.
Rate limits
| Limit | Window | Scope | Routes |
|---|---|---|---|
| 200 requests | 60 seconds | IP address, with Express trust proxy enabled | /api/* |
When the limit is exceeded, Veyra returns 429 with RATE_LIMITED. The implementation enables standard rate-limit headers and disables legacy headers.
Get user
Returns the tracked Discord user's current presence and supported profile information.
| Parameter | Type | Required | Nullable | Description |
|---|---|---|---|---|
| userId | string | Yes | No | Discord snowflake, validated as 17 to 20 digits. |
{
"success": true,
"data": {
"kv": {},
"discord_user": {
"id": "123456789012345678",
"username": "example",
"global_name": "Example",
"display_name": "Example",
"discriminator": "0",
"avatar": "avatar_hash",
"avatar_url": "https://cdn.discordapp.com/avatars/...",
"avatar_decoration_data": null,
"banner": null,
"banner_url": null,
"accent_color": null,
"public_flags": 4194304,
"badges": ["ACTIVE_DEVELOPER"],
"primary_guild": null
},
"activities": [],
"discord_status": "online",
"active_on_discord_web": false,
"active_on_discord_desktop": true,
"active_on_discord_mobile": false,
"active_on_discord_embedded": false,
"active_on_discord_vr": false,
"listening_to_spotify": false,
"spotify": null,
"custom_status": null,
"guild": {
"id": "1399305230268760186",
"nickname": null,
"joined_at": "2026-01-01T00:00:00.000Z"
},
"last_updated": "2026-01-01T00:00:00.000Z",
"tracked_since": "2026-01-01T00:00:00.000Z"
}
}Try it
This explorer calls the live public endpoint from your browser. It does not use credentials.
{}Health JSON
GET /health serves the human status page. GET /health/json returns machine-readable health data.
| Field | Type | Description |
|---|---|---|
| status | string | ok when database and Discord are ready, otherwise degraded. |
| service | string | Always Veyra. |
| discord | boolean | Whether the Discord client has emitted ready. |
| database | boolean | Whether Mongoose is connected. |
| badge_api | boolean | Whether DISCORD_USER_TOKEN is configured server-side. |
| uptime | number | Process uptime in seconds. |
| tracked_users | number | null | User document count when database is connected. |
| ws_connections | number | null | Current WebSocket client count when available. |
| timestamp | string | ISO timestamp generated at response time. |
WebSocket overview
Connect to wss://veyra-db3d.onbelmo.uk/socket. The server sends a hello payload, then clients subscribe to up to 50 valid Discord snowflakes per initialize message.
const userId = "YOUR_DISCORD_USER_ID";
let heartbeat;
function connect() {
const ws = new WebSocket("wss://veyra-db3d.onbelmo.uk/socket");
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
if (msg.op === 1) {
ws.send(JSON.stringify({ op: 2, d: { subscribe_to_ids: [userId] } }));
heartbeat = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 3 }));
}, msg.d.heartbeat_interval);
}
if (msg.op === 0 && msg.t === "INIT_STATE") {
console.log(msg.d[userId] || "User is not tracked");
}
if (msg.op === 0 && msg.t === "PRESENCE_UPDATE") {
console.log(msg.d.discord_status, msg.d.activities);
}
if (msg.op === 0 && msg.t === "USER_REMOVED") {
console.log("Tracking removed for", msg.d.user_id);
}
});
ws.addEventListener("close", () => {
clearInterval(heartbeat);
setTimeout(connect, 3000);
});
}
connect();WebSocket opcodes
| Op | Name | Direction | Payload |
|---|---|---|---|
| 0 | Event | Server to client | Contains t and d. |
| 1 | Hello | Server to client | { "heartbeat_interval": 30000 }. |
| 2 | Initialize | Client to server | { "subscribe_to_ids": ["id"] }, filtered to valid snowflakes and sliced to 50 IDs. |
| 3 | Heartbeat | Client to server | No data required. Sets the client alive flag. |
| 4 | Unsubscribe | Client to server | { "user_ids": ["id"] }. |
WebSocket events
| Event | When it is sent | Data |
|---|---|---|
| INIT_STATE | After op: 2. | Object keyed by tracked user ID. Missing IDs are not included. |
| PRESENCE_UPDATE | After subscribed user presence, profile, guild display, or badge data changes. | Full serialized User object. |
| USER_REMOVED | When a subscribed user leaves the Veyra guild. | { "user_id": "..." }. |
Reconnection
The server sends WebSocket pings every 30 seconds and terminates clients that do not respond with pong. Browser clients handle ping and pong automatically. Clients should still send op: 3 heartbeat messages at the interval from op: 1.
Invalid JSON closes the connection with code 1003. The WebSocket server is configured with a 1024 byte max payload, so keep subscription messages compact. Re-send op: 2 after every reconnect because subscriptions live only on the current socket.
User object
| Field | Type | Nullable | Description |
|---|---|---|---|
| kv | object | No | Currently serialized as an empty object. |
| discord_user | object | No | Discord profile fields: id, username, global_name, display_name, discriminator, avatar, banner, accent color, public flags, badges, decoration data, and primary guild. |
| activities | array | No | Normalized Discord activities from the presence gateway event. |
| discord_status | string | No | Current Discord status or offline. |
| active_on_discord_web | boolean | No | True when presence.clientStatus.web exists. |
| active_on_discord_desktop | boolean | No | True when presence.clientStatus.desktop exists. |
| active_on_discord_mobile | boolean | No | True when presence.clientStatus.mobile exists. |
| active_on_discord_embedded | boolean | No | True when presence.clientStatus.embedded exists. |
| active_on_discord_vr | boolean | No | True when presence.clientStatus.vr exists. |
| listening_to_spotify | boolean | No | True when a Spotify activity is detected. |
| spotify | object | Yes | Current Spotify track object or null. |
| custom_status | object | Yes | text, emoji, emoji_id, emoji_name. |
| guild | object | No | id, nickname, and joined_at. |
| last_updated | string | No | Date when Veyra last updated the stored record. |
| tracked_since | string | No | Date when the Mongo document was first inserted. |
Presence
Veyra stores the presence status string provided by Discord. The docs intentionally limit status values to the values handled by the implementation and Discord.js presence objects.
| Value | Meaning |
|---|---|
| online | User is online. |
| idle | User is idle. |
| dnd | User is in Do Not Disturb. |
| offline | No presence is available or Veyra defaulted the user to offline. |
Activities
Every raw Discord activity is normalized and returned in activities. Veyra also derives top-level spotify and custom_status fields from matching activities.
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | Yes | Activity ID. |
| name | string | Yes | Activity name. |
| type | number | Yes | Discord activity type: 0 Playing, 1 Streaming, 2 Listening, 3 Watching, 4 Custom Status, 5 Competing. |
| state | string | Yes | Activity state line. |
| details | string | Yes | Activity detail line. |
| application_id | string | Yes | Discord application ID. |
| timestamps | object | Yes | start and end in Unix milliseconds. |
| assets | object | Yes | large_image, large_text, small_image, small_text. |
| party | object | Yes | id and size. |
| sync_id | string | Yes | Used by Spotify activities. |
| flags | number | Yes | Raw activity flags. |
| created_at | number | Yes | Activity creation timestamp in Unix milliseconds. |
| buttons | string[] | No | Button labels, defaulting to an empty array. |
Spotify
Spotify is detected when an activity has name === "Spotify", type === 2, and a syncId. When absent, listening_to_spotify is false and spotify is null.
| Field | Type | Nullable | Description |
|---|---|---|---|
| track_id | string | Yes | Spotify sync ID from Discord. |
| timestamps.start | number | Yes | Track start time in Unix milliseconds. |
| timestamps.end | number | Yes | Expected track end time in Unix milliseconds. |
| song | string | Yes | Track name from activity details. |
| artist | string | Yes | Activity state with semicolons replaced by commas. |
| album | string | Yes | Activity large asset text. |
| album_art_url | string | Yes | Resolved from Discord's Spotify asset URL or spotify: asset hash. |
Badges
Badges are returned in discord_user.badges. They are derived from Discord public flags, and when a server-side DISCORD_USER_TOKEN is configured, from Discord profile data such as purchased flags and premium fields.
| Badge | Source in code |
|---|---|
| DISCORD_EMPLOYEE | PUBLIC_FLAGS |
| PARTNERED_SERVER_OWNER | PUBLIC_FLAGS |
| HYPESQUAD_EVENTS | PUBLIC_FLAGS |
| BUG_HUNTER_LEVEL_1 | PUBLIC_FLAGS |
| HOUSE_BRAVERY | PUBLIC_FLAGS |
| HOUSE_BRILLIANCE | PUBLIC_FLAGS |
| HOUSE_BALANCE | PUBLIC_FLAGS |
| EARLY_SUPPORTER | PUBLIC_FLAGS |
| BUG_HUNTER_LEVEL_2 | PUBLIC_FLAGS |
| VERIFIED_BOT_DEVELOPER | PUBLIC_FLAGS |
| ACTIVE_DEVELOPER | PUBLIC_FLAGS |
| NITRO_CLASSIC | PURCHASED_FLAGS or premium_type |
| NITRO | PURCHASED_FLAGS or premium_type |
| NITRO_BASIC | PURCHASED_FLAGS or premium_type |
| SERVER_BOOSTER | premium_guild_since |
| NITRO_SUBSCRIBER | premium_since |
Implementation note: a separate badge router exists in the repository, but server.js does not mount it. This documentation therefore treats badges as fields on the active user endpoint.
Errors
| Error | Status | Meaning | Solution |
|---|---|---|---|
| INVALID_ID | 400 | The path user ID is not 17 to 20 digits. | Pass a valid Discord snowflake. |
| BAD_REQUEST | 400 | The request path could not be decoded. | Use a normal encoded URL path. |
| FORBIDDEN | 403 | The path looked like traversal. | Do not request paths containing parent-directory traversal. |
| USER_NOT_TRACKED | 404 | No user document exists for that ID. | Join and complete the Veyra Discord process, then retry after tracking starts. |
| NOT_FOUND | 404 | The requested route or protected backend file is not public. | Use a documented route. |
| RATE_LIMITED | 429 | The IP exceeded 200 requests in 60 seconds for /api/*. | Back off and retry after the window resets. |
| SERVER_ERROR | 500 | An unexpected server error occurred. | Retry later or check the status page. |
OpenAPI
The rebuilt docs include OpenAPI files for the active REST surface only. Every response includes real, filled-in examples (not just schema), plus per-endpoint auth and rate-limit notes.
Open the interactive API explorer · Download openapi.json or download openapi.yaml.
JavaScript guide
Use direct HTTP requests. Veyra does not include an official SDK in the provided source.
async function getVeyraUser(userId) {
const res = await fetch(`https://veyra-db3d.onbelmo.uk/api/user/${userId}`);
const body = await res.json();
if (!body.success) throw new Error(body.error.code);
return body.data;
}
const user = await getVeyraUser("YOUR_DISCORD_USER_ID");
document.querySelector("[data-name]").textContent =
user.discord_user.global_name || user.discord_user.username;TypeScript guide
Define local types from the documented response fields. No official TypeScript SDK is present in the repository.
type VeyraError = { success: false; error: { code: string; message: string } };
type VeyraSuccess = { success: true; data: { discord_status: string; listening_to_spotify: boolean } };
async function fetchPresence(userId: string): Promise<VeyraSuccess["data"]> {
const response = await fetch(`https://veyra-db3d.onbelmo.uk/api/user/${userId}`);
const body = (await response.json()) as VeyraSuccess | VeyraError;
if (!body.success) throw new Error(body.error.message);
return body.data;
}Python guide
Python applications can use the REST endpoint for snapshots. Use a WebSocket client library when you need live updates.
import requests
def get_veyra_user(user_id: str) -> dict:
response = requests.get(
f"https://veyra-db3d.onbelmo.uk/api/user/{user_id}",
timeout=10,
)
body = response.json()
if not body["success"]:
raise RuntimeError(body["error"]["code"])
return body["data"]React guide
For React or Next.js, fetch Veyra data from a component or your own server route. Do not add private Discord tokens to browser code.
import { useEffect, useState } from "react";
export function VeyraStatus({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://veyra-db3d.onbelmo.uk/api/user/${userId}`)
.then((res) => res.json())
.then((body) => body.success && setUser(body.data));
}, [userId]);
if (!user) return null;
return <p>{user.discord_user.username}: {user.discord_status}</p>;
}Migrating from Lanyard
| Lanyard concept | Veyra equivalent | Notes |
|---|---|---|
/v1/users/:id | /api/user/:userId | Veyra uses a different base URL and path. |
| Discord ID lookup | Tracked Veyra user lookup | Veyra returns USER_NOT_TRACKED when no stored user exists. |
| Presence over WebSocket | wss://veyra-db3d.onbelmo.uk/socket | Veyra uses numeric opcodes 1, 2, 3, and 4 plus event opcode 0. |
| Response fields | Veyra's serialized User object | Map fields deliberately. Do not assume one-to-one parity. |
Troubleshooting
| Problem | What to check |
|---|---|
USER_NOT_TRACKED | Confirm the user joined the official Veyra Discord, completed server verification, and did not leave. Wait for the 5 second join delay or the next startup sync. |
Spotify is null | The user is not currently exposing a Spotify activity matching Veyra's detector. |
| Presence is not updating | Confirm the Discord bot is ready on /health/json and that the user remains in the Veyra guild. |
| WebSocket disconnects | Reconnect, resubscribe, and keep sending op: 3 heartbeats. Keep messages below 1024 bytes. |
| HTTP 429 | Back off. The API limit is 200 requests per 60 seconds per IP. |
/info has no API URL | The command returns the API URL only for users found in the database. |
FAQ
Can I query any Discord ID?
No. Veyra only returns data for tracked users stored by the service.
Does Veyra require authentication?
No public REST or WebSocket authentication is implemented in the provided source.
Where do I find my API link?
Run /info in the Veyra Discord. The bot displays BASE_URL/api/user/:userId.
Does Veyra have official SDKs?
No SDK package is present in the repository. Use direct REST or WebSocket calls.
What does USER_NOT_TRACKED mean?
Veyra has no stored record for that user. Join and complete the Veyra Discord process so tracking can start.