Your own server

Service accounts

Keys for your own servers and tools, with exactly the permissions they need.

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.