Developer API
Endpoint Reference

Endpoint Reference

Every endpoint listed here works with a guild API key. Each entry shows the HTTP method, the path, and an example response.

Replace :guildId with your Discord guild ID. Replace other path params with the relevant ID.

All examples show the success envelope. Error responses follow the shape on the Errors page.


Lookup

Lookup a Discord user

Find which Roblox account a Discord user is linked to in your guild.

GET /lookup/discord/:discordId
X-Guild-ID: YOUR_GUILD_ID

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "discord_id": "123456789012345678",
    "roblox_id": 261,
    "roblox_username": "Shedletsky",
    "verified_at": "2026-04-12T18:33:21Z"
  }
}

Lookup a Roblox user

The reverse direction.

GET /lookup/roblox/:robloxId
X-Guild-ID: YOUR_GUILD_ID

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "discord_id": "123456789012345678",
    "roblox_id": 261,
    "roblox_username": "Shedletsky",
    "verified_at": "2026-04-12T18:33:21Z"
  }
}

Batch lookup

Look up many users in one call. Pass an array of either Discord or Roblox IDs.

POST /lookup/status
{
  "guild_id": "YOUR_GUILD_ID",
  "discord_ids": ["123456789012345678", "234567890123456789"]
}

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "results": [
      { "discord_id": "123456789012345678", "verified": true, "roblox_id": 261 },
      { "discord_id": "234567890123456789", "verified": false }
    ]
  }
}

Reverse lookup

Same as batch lookup but always returns the full link records.

POST /reverse
{ "guild_id": "YOUR_GUILD_ID", "roblox_ids": [261] }

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "results": [
      { "roblox_id": 261, "discord_id": "123456789012345678", "roblox_username": "Shedletsky" }
    ]
  }
}

Bindings (Roblox group rank to Discord role)

List bindings

GET /bindings/list
X-Guild-ID: YOUR_GUILD_ID

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "bindings": [
      {
        "id": 12,
        "type": "group_rank",
        "group_id": 7384293,
        "rank": 200,
        "role_id": "987654321098765432"
      }
    ]
  }
}

Add binding

POST /bindings/add
{
  "guild_id": "YOUR_GUILD_ID",
  "type": "group_rank",
  "group_id": 7384293,
  "rank": 200,
  "role_id": "987654321098765432"
}

Response

{
  "status": "success",
  "code": 200,
  "message": "Binding created",
  "data": { "id": 12 }
}

Update binding

PATCH /bindings/update
{ "binding_id": 12, "rank": 250 }

Response

{ "status": "success", "code": 200, "message": "Binding updated" }

Remove binding

DELETE /bindings/remove/:bindingId

Response

{ "status": "success", "code": 200, "message": "Binding removed" }

Test a binding

Dry-run a binding spec against a user without saving it.

POST /bindings/test
{
  "guild_id": "YOUR_GUILD_ID",
  "discord_id": "123456789012345678",
  "type": "group_rank",
  "group_id": 7384293,
  "rank": 200
}

Response

{
  "status": "success",
  "code": 200,
  "data": { "matches": true, "current_rank": 250 }
}

Validate a binding

Check that a binding spec is well-formed before saving.

POST /bindings/validate
{ "type": "group_rank", "group_id": 7384293, "rank": 200, "role_id": "987654321098765432" }

Response

{ "status": "success", "code": 200, "data": { "valid": true } }

Sync (re-evaluate a user's roles)

Sync one user

POST /interactions/sync
{ "guild_id": "YOUR_GUILD_ID", "user_id": "123456789012345678" }

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "added_roles": ["987654321098765432"],
    "removed_roles": [],
    "nickname_updated": true
  }
}

Sync all users

Triggers a full guild re-evaluation. Very heavy. Limited to 1 call per hour per guild.

POST /interactions/sync/all
{ "guild_id": "YOUR_GUILD_ID" }

Response

{
  "status": "success",
  "code": 200,
  "data": { "queued": true, "members": 4128 }
}

Assign a role manually

POST /interactions/roles/assign
{ "guild_id": "YOUR_GUILD_ID", "user_id": "123...", "role_id": "987..." }

Response

{ "status": "success", "code": 200, "message": "Role assigned" }

Remove a role manually

POST /interactions/roles/remove
{ "guild_id": "YOUR_GUILD_ID", "user_id": "123...", "role_id": "987..." }

Response

{ "status": "success", "code": 200, "message": "Role removed" }

Guild basics

List guild roles

The Discord roles in your guild. Useful for picking the right role for a binding.

GET /guilds/:guildId/roles

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "roles": [
      { "id": "987654321098765432", "name": "Verified", "color": 5763719, "position": 12 }
    ]
  }
}

List guild members (verified)

GET /guilds/:guildId/members

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "members": [
      { "discord_id": "123...", "roblox_id": 261, "roblox_username": "Shedletsky" }
    ],
    "total": 4128
  }
}

List verified users

Shorter version of the above. Just verified records.

GET /guilds/:guildId/verified-users

Response

{
  "status": "success",
  "code": 200,
  "data": [
    { "discord_id": "123...", "roblox_id": 261, "roblox_username": "Shedletsky" }
  ]
}

Audit logs

List audit log entries

GET /guilds/:guildId/audit-logs

Optional query params: ?limit=50&cursor=<id>&action=<type>.

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "entries": [
      {
        "id": 9012,
        "action": "ban.create",
        "actor_id": "123...",
        "target_id": "234...",
        "metadata": { "reason": "Exploiting" },
        "created_at": "2026-05-08T14:22:01Z"
      }
    ],
    "next_cursor": null
  }
}

Write a custom audit entry

Your own integration can append entries. Useful for surfacing third-party actions in the dashboard log feed.

POST /guilds/:guildId/audit-logs
{ "action": "custom.deploy", "metadata": { "build": "v1.42.0" } }

Response

{ "status": "success", "code": 200, "data": { "id": 9013 } }

Moderation (game-server hot path)

These are the calls a game server makes most often. They check a single user fast, with an aggressive cache layer.

Check ban status

GET /moderation/:guildId/ban-status/:identifier

:identifier is either a Discord ID (string) or Roblox user ID (number). Optional query: ?place_id=<placeId> to scope to a specific game.

Response (banned)

{
  "status": "success",
  "code": 200,
  "message": "User is banned",
  "data": {
    "banned": true,
    "ban_info": {
      "id": 1247,
      "reason": "Exploiting",
      "moderator_id": "123...",
      "expires_at": null,
      "created_at": "2026-04-12T18:33:21Z"
    }
  }
}

Response (not banned)

{
  "status": "success",
  "code": 200,
  "message": "User is not banned",
  "data": { "banned": false }
}

Check mute status

GET /moderation/:guildId/mute-status/:identifier

Response (muted)

{
  "status": "success",
  "code": 200,
  "data": {
    "muted": true,
    "mute_info": {
      "id": 412,
      "reason": "Spam",
      "expires_at": "2026-05-09T00:00:00Z"
    }
  }
}

List active bans

Use this on game-server start to seed your local cache.

GET /moderation/:guildId/roblox/bans/active

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "bans": [
      { "id": 1247, "identifier": "261", "reason": "Exploiting", "expires_at": null }
    ]
  }
}

List active mutes

GET /moderation/:guildId/roblox/mutes/active

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "mutes": [
      { "id": 412, "identifier": "261", "reason": "Spam", "expires_at": "2026-05-09T00:00:00Z" }
    ]
  }
}

Push a ban from the game server

When a moderator bans someone in-game, mirror it back to Technified so the dashboard and Discord side stay in sync.

POST /moderation/:guildId/roblox/ban
{
  "identifier": "261",
  "reason": "Exploiting",
  "moderator_id": "123456789012345678",
  "duration_seconds": null,
  "place_id": 1818
}

Response

{
  "status": "success",
  "code": 200,
  "data": { "id": 1247, "banned": true }
}

Push an unban

POST /moderation/:guildId/roblox/unban
{ "identifier": "261", "moderator_id": "123..." }

Response

{ "status": "success", "code": 200, "data": { "unbanned": true } }

Push a mute

POST /moderation/:guildId/roblox/mute
{
  "identifier": "261",
  "reason": "Spam",
  "moderator_id": "123...",
  "duration_seconds": 3600
}

Response

{ "status": "success", "code": 200, "data": { "id": 412, "muted": true } }

Push an unmute

POST /moderation/:guildId/roblox/unmute
{ "identifier": "261", "moderator_id": "123..." }

Response

{ "status": "success", "code": 200, "data": { "unmuted": true } }

Technified Shield (cross-guild flagged users)

Any guild API key can read the Shield list. Only allowlisted guilds can write to it.

Check one user

GET /shield/check/:robloxId

Response (flagged)

{
  "status": "success",
  "code": 200,
  "data": {
    "flagged": true,
    "flag": {
      "roblox_id": "261",
      "roblox_username": "Shedletsky",
      "reason": "User is part of a condo group (12345678)",
      "flagged_by": "Technified",
      "source": "manual",
      "flagged_at": "2026-04-12T18:33:21Z"
    }
  }
}

Response (clean)

{ "status": "success", "code": 200, "data": { "flagged": false } }

Check up to 200 users at once

POST /shield/batch
{ "roblox_ids": [261, 1, 156] }

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "flagged": [
      { "roblox_id": "261", "roblox_username": "Shedletsky", "reason": "User is part of a condo group (12345678)", "source": "manual" }
    ]
  }
}

Bulk import (allowlisted guilds only)

POST /shield/import
{
  "users": [
    { "roblox_id": 261, "reason": "User is part of a condo group (12345678)" }
  ]
}

Response

{ "status": "success", "code": 200, "data": { "imported": 1, "skipped": 0 } }

Staff (basic)

List staff members

GET /staff/members/:guildId

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "members": [
      {
        "discord_id": "123...",
        "roblox_id": 261,
        "role_id": "987...",
        "role_name": "Moderator",
        "on_duty": false
      }
    ]
  }
}

Get one staff member's permissions

GET /staff/permissions/:guildId/:userId

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "user_id": "123...",
    "role_name": "Moderator",
    "permissions": ["moderation.read", "moderation.write", "lookup.read"]
  }
}

Get staff profile

Combined info for one staff member.

GET /guilds/:guildId/staff/profile/:userId

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "discord_id": "123...",
    "role_name": "Moderator",
    "joined_at": "2025-09-12T08:00:00Z",
    "actions_total": 482,
    "sessions_total": 73,
    "minutes_active": 4811,
    "open_loa": null,
    "active_strikes": 0
  }
}

Recent actions

GET /staff/activity/:guildId

Optional ?limit=50&cursor=<id>.

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "actions": [
      {
        "id": 9012,
        "staff_id": "123...",
        "action_type": "ban",
        "target_id": "261",
        "created_at": "2026-05-08T14:22:01Z"
      }
    ],
    "next_cursor": null
  }
}

Aggregate stats

GET /staff/stats/:guildId

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "total_actions": 12048,
    "total_sessions": 1421,
    "total_minutes": 89321,
    "by_type": { "ban": 482, "mute": 1207, "warn": 3091 }
  }
}

Leaderboard

GET /staff/leaderboard/:guildId

Optional ?period=week|month|all.

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "period": "week",
    "leaderboard": [
      { "discord_id": "123...", "role_name": "Moderator", "score": 412, "actions": 38 }
    ]
  }
}

List action types

The set of action_type values used by the activity log.

GET /staff/action-types

Response

{
  "status": "success",
  "code": 200,
  "data": ["ban", "unban", "mute", "unmute", "warn", "kick", "custom"]
}

Staff: LOA (Leave of Absence)

List LOAs

GET /guilds/:guildId/staff/loa

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "loas": [
      {
        "id": 71,
        "staff_id": "123...",
        "starts_at": "2026-05-10T00:00:00Z",
        "ends_at": "2026-05-17T00:00:00Z",
        "reason": "Vacation",
        "status": "active"
      }
    ]
  }
}

Create LOA

POST /guilds/:guildId/staff/loa
{
  "staff_id": "123...",
  "starts_at": "2026-05-10T00:00:00Z",
  "ends_at": "2026-05-17T00:00:00Z",
  "reason": "Vacation"
}

Response

{ "status": "success", "code": 200, "data": { "id": 71 } }

Update / delete LOA

PATCH /guilds/:guildId/staff/loa/:loaId
DELETE /guilds/:guildId/staff/loa/:loaId

Response

{ "status": "success", "code": 200, "message": "LOA updated" }

Expire finished LOAs

Sweeps any LOA whose ends_at is in the past and marks it expired.

POST /guilds/:guildId/staff/loa/expire

Response

{ "status": "success", "code": 200, "data": { "expired": 3 } }

Staff: Strikes

List strikes

GET /guilds/:guildId/staff/strikes

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "strikes": [
      {
        "id": 41,
        "staff_id": "123...",
        "reason": "Missed quota",
        "severity": 1,
        "expires_at": "2026-08-08T00:00:00Z",
        "active": true
      }
    ]
  }
}

Create strike

POST /guilds/:guildId/staff/strikes
{ "staff_id": "123...", "reason": "Missed quota", "severity": 1 }

Response

{ "status": "success", "code": 200, "data": { "id": 41 } }

Update / remove / delete strike

PATCH /guilds/:guildId/staff/strikes/:strikeId/update
PATCH /guilds/:guildId/staff/strikes/:strikeId
DELETE /guilds/:guildId/staff/strikes/:strikeId

PATCH .../update edits fields. The plain PATCH removes the strike (sets active=false). DELETE wipes it.

Response

{ "status": "success", "code": 200, "message": "Strike removed" }

Expire old strikes

POST /guilds/:guildId/staff/strikes/expire

Response

{ "status": "success", "code": 200, "data": { "expired": 2 } }

Staff: Notes

List, create, update, delete

GET    /guilds/:guildId/staff/notes
POST   /guilds/:guildId/staff/notes
PATCH  /guilds/:guildId/staff/notes/:noteId
DELETE /guilds/:guildId/staff/notes/:noteId
{ "staff_id": "123...", "body": "Trial period extended by 2 weeks" }

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "id": 18,
    "staff_id": "123...",
    "body": "Trial period extended by 2 weeks",
    "author_id": "234...",
    "created_at": "2026-05-08T14:22:01Z"
  }
}

Staff: Quota

List quotas

GET /guilds/:guildId/staff/quota

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "quotas": [
      {
        "id": 4,
        "name": "Monthly bans",
        "metric": "ban",
        "target": 20,
        "period": "month",
        "scope": "role:Moderator"
      }
    ]
  }
}

Create / update / delete quota

POST   /guilds/:guildId/staff/quota
PATCH  /guilds/:guildId/staff/quota/:quotaId
DELETE /guilds/:guildId/staff/quota/:quotaId
{ "name": "Monthly bans", "metric": "ban", "target": 20, "period": "month" }

Response

{ "status": "success", "code": 200, "data": { "id": 4 } }

Quota progress

GET /guilds/:guildId/staff/quota/:quotaId/progress

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "quota_id": 4,
    "rows": [
      { "staff_id": "123...", "current": 12, "target": 20, "percent": 60 }
    ]
  }
}

Pending actions

Some quotas need approval before they count.

GET  /guilds/:guildId/staff/quota-pending
POST /guilds/:guildId/staff/quota-pending/:actionId/approve
POST /guilds/:guildId/staff/quota-pending/:actionId/reject

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "pending": [
      { "id": 88, "staff_id": "123...", "action_type": "custom", "submitted_at": "2026-05-08T14:00:00Z" }
    ]
  }
}

Roblox activity tracking

Drive these from your game's server scripts. They power the staff activity dashboard.

Verify in-game staff

Confirms a Roblox player is staff before granting in-game tools.

POST /staff/roblox/verify
{ "guild_id": "YOUR_GUILD_ID", "roblox_id": 261 }

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "is_staff": true,
    "discord_id": "123...",
    "role_name": "Moderator",
    "permissions": ["kick", "ban", "warn"]
  }
}

Register a game (universe)

Call once per universe so the dashboard can show its name and place ID.

POST /roblox/games/register
{ "guild_id": "YOUR_GUILD_ID", "place_id": 1818, "universe_id": 121, "name": "My Game" }

Response

{ "status": "success", "code": 200, "data": { "registered": true } }

Start a session

Call when a staff member joins the server.

POST /staff/roblox/session/start
{ "guild_id": "YOUR_GUILD_ID", "roblox_id": 261, "place_id": 1818, "server_id": "abc-123" }

Response

{
  "status": "success",
  "code": 200,
  "data": { "session_id": "sess_abc123", "started_at": "2026-05-08T14:22:01Z" }
}

End a session

POST /staff/roblox/session/end
{ "session_id": "sess_abc123" }

Response

{
  "status": "success",
  "code": 200,
  "data": { "session_id": "sess_abc123", "duration_seconds": 1820 }
}

Heartbeat

Send every 30 seconds to keep the session alive.

POST /staff/roblox/heartbeat
{ "session_id": "sess_abc123" }

Response

{ "status": "success", "code": 200, "data": { "alive": true } }

Log a custom action

POST /staff/roblox/action
{
  "guild_id": "YOUR_GUILD_ID",
  "session_id": "sess_abc123",
  "action_type": "custom",
  "target_id": "261",
  "details": { "command": "/freeze" }
}

Response

{ "status": "success", "code": 200, "data": { "id": 9012 } }

Get a Roblox user's permissions in-game

Useful when a player runs an admin command and you need the permission tier.

GET /guilds/:guildId/staff/roblox/:robloxId/permissions

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "roblox_id": 261,
    "is_staff": true,
    "role_name": "Moderator",
    "permissions": ["kick", "ban", "warn"]
  }
}

Server Manager (live commands)

The dashboard can queue commands and your game server polls for them.

Heartbeat

POST /roblox/servers/heartbeat
{
  "server_id": "abc-123",
  "place_id": 1818,
  "universe_id": 121,
  "player_count": 14
}

Response

{ "status": "success", "code": 200, "data": { "ok": true } }

Poll for queued commands

GET /roblox/servers/:serverId/commands

Response

{
  "status": "success",
  "code": 200,
  "data": {
    "commands": [
      {
        "id": "cmd_42",
        "type": "kick",
        "target_id": "261",
        "reason": "Exploiting",
        "issued_at": "2026-05-08T14:22:01Z"
      }
    ]
  }
}

Acknowledge a command

Call after you have applied the command in-game.

POST /roblox/servers/:serverId/commands/:commandId/ack
{ "status": "ok" }

Response

{ "status": "success", "code": 200, "data": { "acked": true } }

Tickets

Create a ticket from an application submission

Triggered when an applicant gets to a stage that needs a private channel.

POST /guilds/:guildId/application-submissions/:submissionId/create-ticket
{ "panel_id": 7 }

Response

{
  "status": "success",
  "code": 200,
  "data": { "ticket_id": 941, "channel_id": "1098765432109876543" }
}

WebSocket (game server)

⚠️

Heads up. Roblox does not currently allow outgoing WebSocket connections from live game servers. This endpoint only works from Roblox Studio, not from a published game. The integration is already wired up on our side, so as soon as Roblox enables WebSockets in production it will work without any code change. For now, use HTTP polling on /roblox/servers/:serverId/commands instead.

Live channel for game-server events. Open with Upgrade: websocket. The API key must match :guildId.

GET /ws/guilds/:guildId/roblox?placeId=1818&serverId=abc-123&universeId=121

After upgrade you get JSON frames like:

{ "type": "moderation.ban", "data": { "identifier": "261", "reason": "Exploiting" } }
{ "type": "command", "data": { "id": "cmd_42", "type": "kick", "target_id": "261" } }

The dashboard equivalent (/ws/guilds/:guildId/live) uses Discord OAuth, not API keys, so it isn't listed here.