New Chat APIs: Teleport Users Who Can Chat Together


[Update] February 13, 2026

TL;DR

  • Today we’re launching a new API method that will let you group and teleport players together who are of a similar age and can chat: TextChatService:GetChatGroupsAsync. Next week, we’ll launch another method for Voice Chat Service: VoiceChatService:GetChatGroupsAsync.
  • These APIs take a list of Player instances as an input and returns a sorted array of arrays of chat group IDs players belong to. Matching Group IDs represent the ability to communicate based on players’ age-check status and settings.
  • To use these API methods you must opt-in through Experience Settings > Communication in Studio.
    • Note: Before you can use these methods, you must agree to using these APIs responsibly and comply with our Terms of Use.
  • These server-side methods return group IDs that are unique for the current universe. Since the chat groups for a user returned by the API could change over time, you should check each user’s chat groups every time they leave and re-enter your experience.

Hi Creators,

As we mentioned in our latest roadmap update, we’re launching two new API methods that will let you group and teleport players together who are of a similar age and can chat. We know how important it is to keep users who can chat together, especially for roleplaying and social games. These new methods are:

  • TextChatService:GetChatGroupsAsync: Returns matching groups for players who can synchronously text chat together. This is now available. Read our documentation here.
  • VoiceChatService:GetChatGroupsAsync: Returns matching groups for players eligible to use Voice Chat together. This will be available next week; we’ll update this post when it is.

How it works:

  1. You must first enable these APIs for your experience by going to Experience Settings > Communication and toggling on “Enable chat & voice groups APIs”.
  • Note: By doing so you must agree to using these APIs responsibly and comply with our Terms of Use.
  • Keep in mind you can only use API responses for their intended use. Storing, aggregating, or cross-referencing data returned from this API such as chat groups to build profiles or infer personal details about users is not permitted.
  1. For both API methods, you provide a list of players as an input, and the API returns multiple arrays. Each array represents a group of users who can text or voice chat with each other based on their age group and chat settings.

These server-side methods return group IDs that are unique for the current universe. Since the chat groups for a user returned by the API could change over time, you should check each user’s chat groups every time they leave and re-enter your experience.

What you can use these APIs for

Queue-Based Matchmaking

If you have a lobby or queue, you can use these APIs to ensure that the group of players you are about to teleport into a match are matched and put on the same team.

The code snippet below waits for players to come to the server and the moment there are 4 players that can make two teams where all the players in each team can chat with their team mates (2vs2) it teleports them.

Queue-Based Matchmaking - Code Snippet

-- Server Script
local Players = game:GetService("Players")
local TextChatService = game:GetService("TextChatService")
local TeleportService = game:GetService("TeleportService")

local TARGET_PLACE_ID = 1234567890
local MATCH_SIZE = 4

local Queue: { Player } = {}

local function removeFromQueue(playersToRemove)
	local remove = {}
	for _, player in ipairs(playersToRemove) do
		remove[player] = true
	end

	local newQueue = {}
	for _, player in ipairs(Queue) do
		if not remove[player] then
			table.insert(newQueue, player)
		end
	end

	Queue = newQueue
end

-- Returns true if two groupId arrays intersect
local function sharesChatGroup(groupsA, groupsB)
	local lookup = {}
	for _, groupId in ipairs(groupsA) do
		lookup[groupId] = true
	end

	for _, groupId in ipairs(groupsB) do
		if lookup[groupId] then
			return true
		end
	end

	return false
end

local function tryCreateMatch()
	if #Queue < MATCH_SIZE then
		return
	end

	-- Fetch chat groups for queued players
	local success, chatGroupsByIndex = pcall(function()
		return TextChatService:GetChatGroupsAsync(Queue)
	end)

	if not success or not chatGroupsByIndex then
		warn("Failed to retrieve chat groups")
		return
	end

	-- chatGroupsByIndex[i] corresponds to Queue[i]
	-- Each entry is an array of groupIds for that player

	-- Find all compatible pairs (indices into Queue)
	local compatiblePairs = {}

	for i = 1, #Queue - 1 do
		for j = i + 1, #Queue do
			if sharesChatGroup(chatGroupsByIndex[i], chatGroupsByIndex[j]) then
				table.insert(compatiblePairs, { i, j })
			end
		end
	end

	-- Combine pairs into a 2v2
	for i = 1, #compatiblePairs - 1 do
		for j = i + 1, #compatiblePairs do
			local pairA = compatiblePairs[i]
			local pairB = compatiblePairs[j]

			-- Ensure all indices are unique
			local used = {
				[pairA[1]] = true,
				[pairA[2]] = true,
			}

			if not used[pairB[1]] and not used[pairB[2]] then

				local matchPlayers = {
					Queue[pairA[1]],
					Queue[pairA[2]],
					Queue[pairB[1]],
					Queue[pairB[2]],
				}

				removeFromQueue(matchPlayers)

				-- Reserve private server
				local accessCode
				local ok, err = pcall(function()
					accessCode = TeleportService:ReserveServer(TARGET_PLACE_ID)
				end)

				if not ok then
					warn("Failed to reserve server:", err)
					return
				end

				local teleportOk, teleportErr = pcall(function()
					TeleportService:TeleportToPrivateServer(
						TARGET_PLACE_ID,
						accessCode,
						matchPlayers
					)
				end)

				if not teleportOk then
					warn("Teleport failed:", teleportErr)
				end

				return
			end
		end
	end
end

local function onPlayerAdded(player)
	table.insert(Queue, player)
	tryCreateMatch()
end

local function onPlayerRemoving(player)
	removeFromQueue({ player })
end

Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)

Custom Server Lists

If your experience features a custom server browser, you can now rank or filter those servers based on which ones contain the most users the local player is eligible to chat with.

  • The code snippet below checks a database of servers and the current amount of compatible players in all chat groups in each server.
  • It counts the number of users that the person can chat with in each server, and then sorts the servers based on that number.

Custom Server List - Code Snippet

-- Server Script
local TextChatService = game:GetService("TextChatService")
local Players = game:GetService("Players")

--[[
	Stubbed universe-wide server state.
	In practice, this data could be stored and updated using MemoryStore
	or another backend system.

	Example shape:
	ActiveServers = {
		{
			serverId = "abc",
                    playing = 18,
                    maxPlayers = 24,
			groupUserCounts = {
				groupA = 12,
				groupB = 5
			}
		},
		{
			serverId = "def",
			playing = 12,
                    maxPlayers = 24,
			groupUserCounts = {
				groupB = 8
			}
		}
	}
]]
local ActiveServers = {}

-- Returns servers ranked by chat compatibility with the player
local function getRankedServersForPlayer(player)
	local success, chatGroups = pcall(function()
		return TextChatService:GetChatGroupsAsync({ player })
	end)

	if not success or not chatGroups then
		warn("Failed to retrieve chat groups for player:", player.UserId)
		return {}
	end

	-- Collect all chat group IDs the player is eligible for
	local playerGroupIds = {}
	for _, groupSet in ipairs(chatGroups[1]) do
		for _, groupId in ipairs(groupSet) do
			playerGroupIds[groupId] = true
		end
	end

	-- Score each server based on compatible users
	local rankedServers = {}

	for _, server in ipairs(ActiveServers) do
		local compatibleUserCount = 0

		for groupId, userCount in pairs(server.groupUserCounts) do
			if playerGroupIds[groupId] then
				compatibleUserCount += userCount
			end
		end

		-- Optional: filter out servers with zero compatible users
		if compatibleUserCount > 0 then
			table.insert(rankedServers, {
				serverId = server.serverId,
				playing = server.playing,
				maxPlayers = server.maxPlayers,
				compatibleUsers = compatibleUserCount,
			})
		end
	end

	-- Rank servers: most compatible users first
	table.sort(rankedServers, function(a, b)
		return a.compatibleUsers > b.compatibleUsers
	end)

	return rankedServers
end

-- TODO: Render servers

Reserved Servers

Automatically group users who have completed an age check into servers with other users who have completed an age check.
The snippet below is a script that gets users who join an experience, finds a reserved server with the most people that they can chat with, then teleports them there.

Reserved Server - Code Snippet

-- Server Script
local TextChatService = game:GetService("TextChatService")
local TeleportService = game:GetService("TeleportService")
local Players = game:GetService("Players")

local TARGET_PLACE_ID = 1234567890 -- Destination place ID

--[[
	Stubbed universe-wide server state.
	In practice, this data could be stored and updated using MemoryStore
	or another backend system.

	Example shape:
	ActiveServers = {
		{
			serverId = "abc",
			accessCode = "reserved-server-code",
			groupUserCounts = {
				groupA = 12,
				groupB = 5
			}
		},
		{
			serverId = "def",
			accessCode = "reserved-server-code",
			groupUserCounts = {
				groupB = 8
			}
		}
	}
]]
local ActiveServers = {}

local function teleportPlayerToBestServer(player)
	local success, chatGroups = pcall(function()
		return TextChatService:GetChatGroupsAsync({ player })
	end)

	if not success or not chatGroups then
		warn("Failed to retrieve chat groups for player:", player.UserId)
		return
	end

	-- Collect all chat group IDs the player is eligible for
	local playerGroupIds = {}
	for _, group in ipairs(chatGroups) do
		for _, groupId in ipairs(group) do
			playerGroupIds[groupId] = true
		end
	end

	local bestServer = nil
	local bestUserCount = 0

	-- Choose the server with the highest number of compatible users
	for _, server in ipairs(ActiveServers) do
		local compatibleUserCount = 0

		for groupId, userCount in pairs(server.groupUserCounts) do
			if playerGroupIds[groupId] then
				compatibleUserCount += userCount
			end
		end

		if compatibleUserCount > bestUserCount then
			bestUserCount = compatibleUserCount
			bestServer = server
		end
	end

	if not bestServer then
		warn("No suitable server found for player:", player.UserId)
		return
	end

	local ok, err = pcall(function()
		TeleportService:TeleportToPrivateServer(
			TARGET_PLACE_ID,
			bestServer.accessCode,
			{ player }
		)
	end)

	if not ok then
		warn("Teleport failed:", err)
	end
end

Players.PlayerAdded:Connect(teleportPlayerToBestServer)

What’s Next

These APIs are just the first step in what we’re doing to keep experiences fun, engaging, and connected. Some other upcoming updates will include:

  • Experience Chat for Trusted Connections - Next Week: Trusted Connections will be able to see each other’s messages in Experience Text and Voice Chat regardless of age group.
  • Preset Chat Messages - Early 2026: Users will be able to use preset chat messages across all age groups to coordinate gameplay.
  • Global Chat - Mid 2026: Users will be able to chat across servers with a new text chat channel.

Tomorrow, we’ll publish our next progress update on our communication roadmap; be sure to check back in!

FAQs

1. What happens if I try to use these APIs without toggling on the setting in Experience Settings?

  • The methods will return an error. You must explicitly consent to using these APIs responsibly by enabling “Enable Text & Voice Chat group APIs” under Experience Settings > Communication.

2. Will these APIs return the age of users?

  • No. The APIs only return groups of users who are eligible to chat together based on their age group and privacy settings. You will not receive specific age data.

3. What happens if I store any information returned by either API after a user has left the experience?

  • The data and chat groups for a user can change over time for a number of different reasons; to make sure you have the latest groups a user can chat with, you should call this method anytime a user leaves then rejoins the experience.
  • Keep in mind you must comply with Roblox’s Terms of Use and you can only use API responses for their intended use. Storing, aggregating, or cross-referencing data returned from this API such as chat groups to build profiles or infer personal details about users is not permitted.
67 Likes

This topic was automatically opened after 10 minutes.

I can’t believe that you’re not undoing this whole age check requirement. No one wants this.

157 Likes

Will you guys just simply look for a better way of dealing with predators already? Your chat update has done NOTHING. KIDS have ALREADY BEEN KIDNAPPED, the age verification has done NOTHING other than harm, you guys are so stubborn

100 Likes

why do you keep putting barely working “solutions” over this update instead of just reverting it

58 Likes

they sadly legally won’t be able to (COPPA)

11 Likes

Okay but I think the biggest struggle with this is global matchmaking still isn’t possible because in that use case we can’t get player instances but instead need to depend on a diffrent method like UserId’s.

Not only that but age based chat is becoming a serious problem for smaller games and games that depend on social interactions.

15 Likes

This update is pretty good compared to what just happened

12 Likes

I’m not sure I understand. Having 9 year olds scan their face to access chat doesn’t seem like compliance with COPPA to me.

37 Likes

companies which allow under 13 users to communicate with 13+ users are required to now make them unable to do that

8 Likes

It actually is because data is NOT retained or used in the training of AI model’s. Hell coppa even has a spesific exemption for age verifcation and biometrics if it’s to allocate a user into locking them out of stuff they should not have access to.

4 Likes

But what about the whole age check data retention? The selfie and/or ID.

5 Likes

it states there that companies have to use selfie age verification

1 Like

source

8 Likes

is only 45% of your DAU verifying something to brag about
image

21 Likes

I wonder how this will be enforced. I’m sure if one were to do this in a silent way, it would be fairly difficult to detect. But interesting nonetheless.

6 Likes

How frequent do you expect this to be? I want to add replying to comments in my game but the rules prohibit this without checking if they can chat, and there is no way for me to do that if both participants are not in the same server. I imagine this API and remembering the information is not viable for that.

3 Likes

So, for an article I spent about 20 hours on, and an API that is potentially way more powerful than the article, the only safeguard against a potential privacy nightmare is a sign saying “don’t do that”. I highly doubt that will stop any of that. Not like it can be done automatically by sifting through stored traffic (DataStores, MemoryStores, HttpService) since the reserved servers use case requires storage. We’ve had trouble getting enforcement for worse.


Besides that, I guess it is nice to see we can much more easily do these types of custom matchmaking. I’m concerned about the compliance part about data storage. For a server list, I would consider storing user ids to be able to priortize playing with friends, but now I’m storing user ids and chat group identfiers together.

Does this also happen to manage blocked players (as in: 2 players are blocked will give different group ids)? I don’t believe we currently have a great way to prevent matchmaking blocked players together.

17 Likes
Do you like the chat update now?
  • Yes, I like this update as of currently
  • Yes, I was against it but not anymore
  • No, They have tried to improve but it’s ruined my experience
  • No, It dangerous, limiting and/or frustrating to develop and use the platform
0 voters
10 Likes

I would hope it would update frequently. For example: birthdays, so changing age groups.

2 Likes