You can track activity either with the downloadable activity loader (see "Set up activity tracking in your game (the loader, rank and idle time)") or by writing your own script against this API. Use this article if you are writing your own.
The downloadable loader does not use this API. It calls older /api/activity endpoints with a separate activity key. Keys made under Settings > Public API do not work with the loader, and the other way round.
Before you start
In Roblox Game Settings turn on Security > Allow HTTP Requests.
Create an API key (see "The Rosterr game API: what it is, how to make an API key and keep it safe").
Put the key in a server-only place, such as a Secret or a module under ServerScriptService.
The script
Put this in a Script in ServerScriptService:
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")local BASE_URL = "https://YOUR-ROSTERR-ADDRESS/api/public/v1/workspace/" .. "YOUR_GROUP_ID"
local API_KEY = "rosterr_..." -- keep server-side onlylocal MIN_RANK = 1 -- only track players at or above this group rank
local GROUP_ID = tonumber("YOUR_GROUP_ID")local function request(method: string, path: string, body: { [string]: any }?)
local ok, result = pcall(function()
return HttpService:RequestAsync({
Url = BASE_URL .. 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("[Rosterr] request failed:", result)
return nil
end if result.StatusCode == 429 then
warn("[Rosterr] rate limited; retry after", result.Headers["retry-after"])
end local decoded = (result.Body ~= "" and select(2, pcall(HttpService.JSONDecode, HttpService, result.Body))) or nil
return result.StatusCode, decoded
endlocal function nowIso(): string
return DateTime.now():ToIsoDate() -- UTC, e.g. 2026-09-19T18:04:05Z
endPlayers.PlayerAdded:Connect(function(player)
if player:GetRankInGroup(GROUP_ID) < MIN_RANK then return end
local status, body = request("POST", ("/user/%d/activity/start"):format(player.UserId), { time = nowIso() })
-- 404: not a workspace member; 409: session already open (e.g. server hop). Both are safe to ignore.
if status and status >= 400 and status ~= 404 and status ~= 409 then
warn("[Rosterr] start failed:", status, body and body.error)
end
end)Players.PlayerRemoving:Connect(function(player)
if player:GetRankInGroup(GROUP_ID) < MIN_RANK then return end
request("POST", ("/user/%d/activity/end"):format(player.UserId), { time = nowIso() })
end)game:BindToClose(function()
for _, player in Players:GetPlayers() do
request("POST", ("/user/%d/activity/end"):format(player.UserId), { time = nowIso() })
end
end)
How it works
Start:
POST /user/{userId}/activity/startwith{ "time": "<UTC ISO 8601>" }. You get 409 if a session is already active for that person.End:
POST /user/{userId}/activity/endwith the same body. You get 404 if no session is active.userIdis the Roblox user ID and must belong to a member of the workspace (otherwise 404).Times are UTC in ISO 8601 format.
Notes
RequestAsync(notGetAsyncorPostAsync) is used so you can read the status code and headers instead of non-2xx responses throwing errors.Roblox limits each server to 500 HTTP requests a minute, and the API allows 200 per key per 5 minutes. So only track staff ranks, and avoid sending anything per chat message.
If a server shuts down,
BindToCloseends open sessions so time is not lost.Roblox Studio is not a real server. Test in a published game.
Related articles
The Rosterr game API: what it is, how to make an API key and keep it safe
Rosterr game API: endpoints reference
Game API errors and rate limits (401, 404, 409, 429 and how to back off)
