Ranking API

Change a Roblox group rank from anywhere (a game server, a Discord bot, a scheduled job) with one authenticated request. The same guardrails and audit trail apply as when you click the button in the dashboard.

How it works

Your service sends a request to Rostack. Rostack authenticates the API key, resolves it to a workspace, applies the guardrails, then calls Roblox's official Open Cloud API using the group's own key. Your service never handles a Roblox credential.

request path

your game / bot
      │  POST /api/v1/rank   (Authorization: Bearer rsk_live_…)
      ▼
   Rostack ── authenticate key ── check guardrails ── write audit entry
      │
      │  assignRole  (x-api-key: your group's Open Cloud key)
      ▼
   Roblox Open Cloud ── rank changed

Because both the dashboard and the API funnel through the same routine, a rank change made by a bot is indistinguishable in the audit log from one made by a person, except that the actor is recorded as the key that did it.

Setup

  1. 1

    Connect a Roblox Open Cloud key

    In your workspace, open Ranking and connect an Open Cloud API key created at Creator Dashboard → API Keys. It needs the group-membership API with your group added, and both group:read and group:write. Rostack encrypts it with Cloud KMS and stores only the ciphertext.

  2. 2

    Create a Rostack API key

    On the same page, create a key and give it a max rank. The key can never assign a rank at or above that number, no matter what it asks for.

    Copy it immediately

    The key is shown once and stored only as a hash. If you lose it, revoke it and create another. Nobody, including us, can recover the original.
  3. 3

    Store the key as a secret

    Put it in an environment variable or Roblox secret store, never in a committed script. Anyone holding the key can rank up to its ceiling.

  4. 4

    Send your first request

    Call the endpoint below. A 200 means Roblox has already applied the change.

The endpoint

POST https://rostack.app/api/v1/rank
Authorization: Bearer rsk_live_…
Content-Type: application/json

Request body:

FieldTypeRequiredDescription
robloxUserIdnumberYesThe Roblox user to rank.
ranknumberYesTarget rank, 0–255. Must match a role that exists in your group.
reasonstringNoRecorded in the audit log. Write something a human will understand later.

A successful response:

200 OK

{
  "ok": true,
  "robloxUserId": 3961562630,
  "rank": 50,
  "roleName": "Moderator"
}

Examples

Luau (Roblox game server)

-- Roblox game server. HttpService must be enabled.
local HttpService = game:GetService("HttpService")

local ROSTACK_KEY = "rsk_live_xxx" -- store this in a secret, not in a script

local function setRank(userId: number, rank: number, reason: string)
	local ok, res = pcall(function()
		return HttpService:RequestAsync({
			Url = "https://rostack.app/api/v1/rank",
			Method = "POST",
			Headers = {
				["Authorization"] = "Bearer " .. ROSTACK_KEY,
				["Content-Type"] = "application/json",
			},
			Body = HttpService:JSONEncode({
				robloxUserId = userId,
				rank = rank,
				reason = reason,
			}),
		})
	end)

	if not ok then
		warn("rostack request failed:", res)
		return false
	end
	if not res.Success then
		warn("rostack rejected:", res.StatusCode, res.Body)
		return false
	end
	return true
end

setRank(3961562630, 50, "Reached 100 activity points")

TypeScript (Discord bot or worker)

// Discord bot, worker, or any Node service.
const res = await fetch("https://rostack.app/api/v1/rank", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROSTACK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    robloxUserId: 3961562630,
    rank: 50,
    reason: "Passed the moderator interview",
  }),
});

const body = await res.json();
if (!res.ok) {
  // body.error is a stable machine code; body.message is for humans.
  throw new Error(`${body.error}: ${body.message}`);
}
console.log(`Now ${body.roleName} (rank ${body.rank})`);

curl

curl -X POST https://rostack.app/api/v1/rank \
  -H "Authorization: Bearer rsk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "robloxUserId": 3961562630,
    "rank": 50,
    "reason": "Reached 100 activity points"
  }'

Errors

Every failure returns a stable error code and a human message. Branch on the code, show the message.

StatusCodeWhat it means
401unauthorizedThe key is unknown or revoked. Deliberately the same message for both. Otherwise the endpoint could be used to probe which keys once existed.
403rank_above_key_ceilingThe requested rank is at or above the key's max rank.
403roblox_unauthorizedThe workspace has no Open Cloud key connected, or Roblox rejected it.
400rank_rule_invalidNo role exists at that rank, or the rank is the owner (255).
400invalid_requestThe body did not validate. The response lists the expected shape.

Guardrails

These are enforced server-side on every call, including from the dashboard. They cannot be turned off, because the failure they prevent is not recoverable.

RuleWhy
Rank 255 can never be assigned255 is the group owner. Transferring ownership is not something a bot should be able to do by sending a number.
A key cannot assign at or above its own ceilingA leaked game-server key should be able to promote a moderator, not create an administrator.
The target role must existRoblox would otherwise silently accept a rank with no matching role.
Every attempt is written to the audit log before it runsA crash mid-call still leaves evidence that it was attempted, with the actor and reason.

Rate limits

Roblox caps group membership writes at 300/min per API key. Rostack paces below that ceiling, so a burst of API calls is queued rather than throttled by Roblox. Expect a large batch to take minutes, not seconds.

Rotating a key

Revoking is immediate and irreversible. Revoked keys are kept, not deleted, so the audit log can still resolve which key made a past change.