Guide

Samples you can paste straight into Roblox. ⚠️ First, enableAllow HTTP Requestsunder Game Settings > Security.

Where to connect

https://www.vespo.app/v1/…
Authorization: Bearer <your API key>

You get a key the moment you register on thedeveloper site.

Know this first: the player's side

  • Players see it once they sign in to Vespo with Roblox.

    They open the app (Android) or the web version at https://robverse-app.expo.app and press "Continue with Roblox". Anyone who has spoken in the game even once is already registered, so their chats appear the moment they sign in. You do not have to register or invite anyone for them.

  • Only people who installed Vespo and turned on notifications get notified.

    On phones that means people who installed the app and allowed notifications; on the web version, people who allowed browser notifications. People who have not installed it get nothing (in-game chat keeps working as before).

  • Replies written in the app do not appear inside the game.

    Roblox's rules do not allow text written outside to be brought into the game. Only leaving, renaming and the icon background colour flow back (see "7. Receive actions taken in Vespo"). Think of it as "readable and noticeable outside the game", not "replies show up in the game".

1. Where to put it, and the key

⚠️ Keep the key on the server side only. Put it in a LocalScript and anyone can read it.

-- Put this in ServerScriptService (where clients cannot see it)
local HttpService = game:GetService("HttpService")

local BASE = "https://www.vespo.app"
-- ⚠️ Do not hard-code the key. Reading it from Creator Dashboard Secrets
--    or a private value in ServerStorage is safer
local API_KEY = "your key here"

local function call(method, path, body)
	local ok, res = pcall(function()
		return HttpService:RequestAsync({
			Url = BASE .. path,
			Method = method,
			Headers = {
				["Authorization"] = "Bearer " .. API_KEY,
				["Content-Type"] = "application/json",
			},
			Body = body and HttpService:JSONEncode(body) or nil,
		})
	end)
	if not ok then
		warn("[Vespo] request failed:", res)
		return nil
	end
	-- ⚠️ Anything outside the 200s carries an error in the body. It is also kept in the "Failures" tab on the developer site
	if res.StatusCode >= 400 then
		warn("[Vespo] " .. res.StatusCode .. " " .. tostring(res.Body))
		return nil
	end
	return HttpService:JSONDecode(res.Body)
end

2. Create a chat room

Calling it again gives the same result (the same ID is never recreated). The returned created tells you whether it was just made. ⚠️ roomId is one namespace shared by all developers. Whoever creates it first owns it; using the same ID later returns 403. Prefix it with your game's name (e.g. myGame:lobby).

-- roomId is a string you choose. Remember the same ID on the game side too
local res = call("POST", "/v1/rooms", {
	roomId = "myGame:lobby",
	name = "Lobby",
	members = { 12345678, 87654321 },  -- Roblox UserIds
	-- The icon is optional. You can use anything from the Roblox catalog
	icon = { id = 123456789, thumb = "Asset" },  -- "Asset" or "Bundle"
	iconBg = "#ffd9a0",
})

-- res.created is true if this call just created it
print(res and res.created)

3. Send a message

Sending also notifies people who have closed the game, on their phone. ⚠️ The body goes in content (not text). Add a clientMsgId and a resend will not be inserted twice.

local Players = game:GetService("Players")

-- ⚠️ presentRobloxIds = people on this same server right now.
--    They can already see it in the game, so their phones stay quiet
local present = {}
for _, p in Players:GetPlayers() do
	table.insert(present, p.UserId)
end

call("POST", "/v1/messages", {
	roomId = "myGame:lobby",
	senderUserId = player.UserId,
	senderName = player.DisplayName,
	content = message,                  -- ⚠️ up to 2000 characters
	clientMsgId = HttpService:GenerateGUID(false),
	experienceId = game.GameId,
	presentRobloxIds = present,
})

4. Report who is here

⚠️ Send everyone every time, not a diff. A dropped call then leaves no drift. The first call links the game automatically and your concurrent-player count appears. ⭐ **Do this first**: only players you have reported here can be added to rooms or speak (report on join too).

local Players = game:GetService("Players")

local function report()
	local ids = {}
	for _, p in Players:GetPlayers() do
		table.insert(ids, p.UserId)
	end
	call("POST", "/v1/presence", {
		experienceId = game.GameId,
		experienceName = "your game name",
		placeId = game.PlaceId,
		serverId = game.JobId,
		robloxUserIds = ids,
	})
end

-- ⭐ Also report the moment someone joins, so they can chat right away (no 30s wait)
Players.PlayerAdded:Connect(report)
task.spawn(function()
	while task.wait(30) do
		report()
	end
end)

-- ⚠️ When the server shuts down, send an empty list = everyone in that room leaves
game:BindToClose(function()
	call("POST", "/v1/presence", {
		experienceId = game.GameId,
		serverId = game.JobId,
		robloxUserIds = {},
	})
end)

5. Let players choose whether to show "now playing"

⚠️ **Off by default.** Unless you call this endpoint, nobody sees that someone is playing your game (it is the player's choice, so we cannot turn it on for them).

-- Put a "show that I'm playing" switch inside your game and call this when it is pressed
call("POST", "/v1/presence-settings", {
	robloxUserId = player.UserId,
	show = true,                   -- false hides it from everyone again
	name = "your game name",       -- what to show (defaults to the game name)
})

-- ⚠️ Shown only to people in the same chats and friends. Not to everyone.
-- ⚠️ The switch belongs **in your game**. RobVerse has nowhere to show it.
-- ⚠️ Do not call this for people who did not press it (you would overwrite their choice).

6. Leave a room

Call this when someone leaves on the game side. They disappear from the list in Vespo too.

call("POST", "/v1/rooms/leave", {
	roomId = "myGame:lobby",
	robloxUserId = player.UserId,
})

7. Receive actions taken in Vespo

A message board that tells the game when someone left or renamed a room from the Vespo side (website or phone). ⚠️ It only arrives when you fetch it (Roblox has no way for us to push). Anything that happened during play is applied the next time you call it: on join, or every few minutes.

-- Once on join, for example. Ack each action once you have applied it
local ops = call("GET", "/v1/room-ops?robloxUserId=" .. player.UserId)
for _, op in (ops and ops.ops or {}) do
	-- op.kind is "leave" / "rename" / "icon_bg" and so on
	-- Update your game's own data here

	call("POST", "/v1/room-ops/ack", { ids = { op.id } })
end

What comes back on failure

Anything outside the 200s carries { "error": "…" } in the body. The same record is kept in the "Failures" tab on the developer site.

  • 401 unauthorized

    The key is missing, wrong, or revoked. Check Authorization: Bearer <key>.

  • 429 rate_limited

    Over the per-minute limit (the response carries limitPerMin and retry-after: 60). Check that you are not calling it every frame inside a loop.

  • 400 xxx_invalid

    roomId_invalid / members_invalid / content_invalid / clientMsgId_invalid / senderUserId_invalid / experienceId_invalid / serverId_invalid / icon_invalid / iconBg_invalid / kind_invalid. The name tells you which value is bad. bad_request means the body could not be read as JSON.

  • 403 room_owned_by_other / members_not_present / sender_not_present / not_present / experience_mismatch / app_unlinked

    You cannot add to, write in, or remove people from a room made by another developer (or a chat made in the Vespo app). members_not_present / sender_not_present: you tried to add, or speak as, a player **your game has not reported via /v1/presence in the last 10 minutes** (so nobody can pull players who are not in the game into a chat). Send presence first. "Now playing" can only be toggled by the player who is in that experience right now. You cannot claim an experienceId different from the linked one. app_unlinked means the key is not yet linked to an app.

  • 404 room_not_found / 403 not_a_member

    The room does not exist, or the sender is not a member. Create it with /v1/rooms and add people first.

  • 403 pending_limit / 423 room_frozen

    pending_limit: you exceeded the allowance that applies **while under review** (3 rooms, 10 members per room, 60 calls/min). New registrations start under review; once the Vespo team approves it you get the normal limits (we email you). room_frozen: the team has frozen that chat (it can be read, but not written to).

  • 500 internal_error

    A problem on our side. The details are not returned (they are kept in the "Failures" tab on the developer site).

Limits

  • Rooms

    roomId 64 characters / name 50 characters / members 1–100 (duplicates removed automatically).

  • Messages

    content 2000 characters / clientMsgId 64 characters / senderName 50 characters. The only kind is text (call is reserved for Vespo's own call records).

  • Who is here

    serverId 100 characters / up to 200 robloxUserIds. Everyone every time, not a diff.

  • Message board

    Up to 50 per fetch. Always ack what you applied (otherwise the same ones come back next time).

Things to remember

  • Keep the key on the server only.

    Put it on the client side and anyone can read it.

  • Limits grow with your player count automatically (600 + 1.5 × concurrent players per minute).

    Getting popular never gets you cut off. ⚠️ The reverse is also true: calling too often with few players returns 429. Do not call every frame inside a loop.

  • The only numbers you can see are totals and trends.

    Who said what is shown to nobody — not to developers, not to us.

Get an API key