For your own game server, build machine or tools: calls made as you, the developer, not as a player. Make one under MGDS > Service accounts, add a key, and tick what it may do in each of your projects. The key is shown once; MGDS keeps only a hash of it. Send it where a player sends their session token:
Authorization: Bearer mgdsk_...
A key never expires. Replace one by adding a second key, moving your server over, and revoking the first. Archiving the service account stops all of its keys until you restore it. Keep keys on your servers only: anything shipped inside a game is readable by its players.
Permissions
Given per project. A call without the permission it needs answers 403.
| Permission | Allows |
|---|---|
players.read |
GET /players, GET /players/{playerId} |
players.sessions |
DELETE /players/{playerId}/sessions |
saves.read |
Reading any save file, private ones too: GET /saves/{id}, its file entries, GET /players/{playerId}/saves (all of them, not only the public ones) |
saves.write |
Everything an owner can do to any save file (writing, entries, uploads, owners), and POST /players/{playerId}/saves |
reports.read |
GET /reports, GET /reports/{id}, GET /reports/{id}/attachments/{attachmentId} |
reports.manage |
POST /reports/{id}/messages, POST /reports/{id}/close |
config.read |
GET /config/environments, GET /config/environments/{environment} |
config.write |
Every call under Remote config below that changes something |
leaderboards.manage |
PUT /leaderboards/{id}: create a board or change its settings and tiers |
leaderboards.reset |
POST /leaderboards/{id}/reset |
leaderboards.delete |
DELETE /leaderboards/{id}, with every version and score |
leaderboards.submit |
POST /leaderboards/{id}/scores/players/{playerId}, on server-only boards too |
leaderboards.scores.delete |
DELETE /leaderboards/{id}/scores/players/{playerId}?version= |
players.export |
GET /players/{playerId}/export: the same document a player gets from /me/export |
players.delete |
DELETE /players/{playerId}: the same deletion as /me, for good |
push.triggers |
PUT /push/triggers/{name}, DELETE /push/triggers/{name} |
push.send |
POST /push/messages, now or scheduled |
push.recall |
DELETE /push/messages/{id}: recall a message that went out, or cancel a scheduled one |
push.read |
GET /push/triggers, GET /push/messages, GET /push/messages/{id} |
Who am I
Not under a project, since one key can reach several:
GET https://www.dasmaffin.com/api/mgds/v1/service-account
{ "id": "...", "name": "Game server",
"projects": [ { "id": "...", "name": "The Hole", "archived": false, "permissions": ["players.read", "saves.read"] } ] }
Players
GET /players?page=1 newest first, 50 to a page
GET /players/{playerId}
DELETE /players/{playerId}/sessions signs them out on every device -> { "ended": 2 }
GET /players/{playerId}/export everything about them (players.export)
DELETE /players/{playerId} them and everything of theirs, for good (players.delete)
{ "page": 1, "pages": 1, "total": 2,
"players": [ { "id": "...", "displayName": "Maffin", "createdAt": "...", "lastSeenAt": "...", "logins": ["sbox"] } ] }
Save files for a player
POST /players/{playerId}/saves
{ "name": "guild", "public": false, "entries": { ... } }
The same body and answer as POST /me/saves, and the file is that player's exactly as if they had
made it: they are its first owner and its createdBy, and the project's limit on files per player
applies. Every other save file call takes the key as it is.
Reports
GET /reports?status=open&since=2026-10-01&unreadOnly=true&player={playerId}&take=50
GET /reports/{id} with its description and messages
GET /reports/{id}/attachments/{attachmentId} the file, as a download
POST /reports/{id}/messages { "body": "Fixed in 1.2.4" } - the player sees it in the game
POST /reports/{id}/close
status is open, closed or any (the default); anything else is a 400. Each report lists
its attachments with their ids and kinds (logs, images, ...). Reading through the API never
marks a report read; that belongs to whoever opens it on the dashboard, where an answer shows which
service account gave it.
Remote config
GET /config/environments [{ id, name, isDefault }]
POST /config/environments { "name": "staging" }
GET /config/environments/{environment} settings and rules, exactly as set up
PUT /config/environments/{environment}/settings/{key} { "type": "float", "value": 2.5 }
{ "type": "image", "media": "<id>" }
DELETE /config/environments/{environment}/settings/{key} and every rule's value for it
POST /config/environments/{environment}/rules a rule, below
PUT /config/rules/{ruleId} the same; fields left out stay
DELETE /config/rules/{ruleId}
POST /config/media multipart, below -> { "media": "<id>", ... }
{environment} is its name or id. A rule:
{ "name": "Fast on Android", "condition": "platform == \"Android\"", "rollout": 100, "priority": 10, "enabled": true,
"startsAt": "2026-11-01T00:00:00Z", "endsAt": null, "overrides": { "speed": 5.0, "banner": { "media": "<id>" } } }
overrides replaces the rule's values when it is sent, and every key in it must be a setting of
that environment, of that setting's type. Changing a setting's type drops every rule's value for it.
Media is uploaded first, already compressed, as multipart/form-data: the file in file, its
contentType (what it is once unpacked), and compression (none or gzip, checked). Optionally
width, height, seconds, originalBytes and encoding (a few words on what was done to it).
An upload no setting or rule uses within a day is deleted.
Leaderboards
PUT /leaderboards/{id} create, or change what is sent
DELETE /leaderboards/{id}
POST /leaderboards/{id}/reset archive now and start an empty version
POST /leaderboards/{id}/scores/players/{playerId} { "score": 1234, "metadata": { ... } }
DELETE /leaderboards/{id}/scores/players/{playerId} the current version, or ?version=3
A board's settings, any left out stay as they are:
{ "name": "Weekly high scores", "sortOrder": "desc", "updateType": "keepBest", "serverOnly": false, "bucketSize": 50,
"reset": "weekly", "resetAt": "00:00", "resetWeekday": 1, "resetDay": 1,
"tiers": [ { "name": "Bronze", "cutoff": 0 }, { "name": "Gold", "cutoff": 5000 } ] }
reset is never, daily, weekly or monthly; resetAt a UTC time; resetWeekday 0 (Sunday)
to 6; resetDay 1 to 28. tiers, when sent, replaces the board's. A score submitted for a player
follows the board's update rule exactly as the player's own would.
Push messages
GET /push/triggers
PUT /push/triggers/{name} { "description": "...", "parameters": [ { "name": "message", "type": "string", "required": true, "description": "..." } ] }
DELETE /push/triggers/{name} with every message sent with it; scheduled ones are cancelled
POST /push/messages send or schedule -> 201, the message
GET /push/messages?environment=&status=&limit=50&before={id}
GET /push/messages/{id}
DELETE /push/messages/{id} recall, or cancel if scheduled -> the message; 409 when it is already over
A parameter's type is string, bool, int, long, float or json. A trigger's name is
fixed once made: PUT to an existing one changes its description and parameters.
{ "trigger": "MaintenanceWarning", "arguments": { "message": "Back in 20 minutes", "minutes": 20 },
"environment": "production", "players": ["8c0e..."], "condition": "platform == \"windows\"",
"signedOut": true, "delivery": "sticky", "sendAt": "2026-11-07T19:40:00Z", "lastsSeconds": 3600 }
Send to players, or to a channel ("channel": "guild-42"), or neither for everyone. Everything
but trigger may be left out: the default environment, everyone, no condition, signed-out games too,
sticky, now, an hour. expiresAt may stand in for lastsSeconds. Arguments are checked against the
trigger: every required one given, none it does not have, each of its type. A message describes
itself with status, connected (games it reached as it went out), delivered and acknowledged.