Generic Game Protocol v1
The Generic Game Protocol has a Protocol Version, a plain integer that covers only the wire format: the message envelope, the handshake, the error shape and event delivery. This page specifies version 1.
A connector that offers no version speaks version 0: the protocol as documented in Adding support for a new game and the Generic Connector Protocol Reference, unchanged. Every version, version 0 included, stays supported indefinitely. Existing connectors keep working without any change, and operators never have to update a connector because Takaro deployed.
New game features never bump the version. They arrive as Capabilities, which a game declares in its Capability Manifest. Version 1 is what lets a connector declare capabilities at all.
What changes in version 1
Section titled “What changes in version 1”| Area | Version 0 | Version 1 |
|---|---|---|
| Handshake | identify without protocolVersions |
identify with protocolVersions and capabilities; identifyResponse names the chosen protocolVersion |
| What the game supports | every action assumed | only what the Capability Manifest declares; anything else is answered unsupported by Takaro |
| Response errors | free-form (error string or type: 'error' frame) |
{ "type": "response", "requestId", "error": { "code", "message" } } with a fixed set of codes |
| Events | fire-and-forget | streamId + seq on every event, cumulative eventAck, resend after reconnect |
| Map | getMapInfo / getMapTile { x, y, z } (7 Days to Die web-map layout) |
map capability with Dimensions; getMapTile { dimension, zoom, x, y }, getMapImage { dimension, offset, length } |
| Positions and inventory | always polled | may be pushed: events.player-positions and events.inventory-changed turn polling off, see Pushed Positions and Pushed inventory changes |
| Game identity | server name, plus the optional game and connector identify fields |
also the game.profile capability (display name, build, icon) and getGameIcon |
Message envelope
Section titled “Message envelope”Every frame is a JSON text message: { "type": string, "payload": object | array | null, "requestId"?: string, "error"?: ... }.
Takaro puts a requestId on every frame it sends. If the frame does not answer something, it is a random UUID. It is only meaningful on request, where you must echo it on your response, and on capabilitiesResponse, where it echoes yours.
| Direction | type |
When |
|---|---|---|
| Takaro → connector | connected |
Right after the socket opens, payload: { "clientId": "client-<uuid>" }. Informational. |
| connector → Takaro | identify |
First thing you send. |
| Takaro → connector | identifyResponse |
Answer to identify. |
| connector → Takaro | capabilities |
Version 1 only: replace the Capability Manifest. |
| Takaro → connector | capabilitiesResponse |
Answer to capabilities. |
| Takaro → connector | request |
Takaro calls an action. Answer every one with a response. |
| connector → Takaro | response |
Answer to a request. |
| connector → Takaro | gameEvent |
Something happened in the game. |
| Takaro → connector | eventAck |
Version 1 only: cumulative acknowledgement of events. |
| Takaro → connector | eventError |
Version 1 only: an event was rejected or failed. |
| Takaro → connector | error |
Generic error, such as a malformed frame, or a response that failed validation (carries the request’s requestId). |
| connector → Takaro | ping |
Optional application ping; Takaro answers pong. |
Takaro also sends WebSocket-level pings every 30 seconds by default. A connection that does not answer with a pong by the next round is terminated. Most WebSocket libraries answer automatically.
Requests versus events. Requests go Takaro → game and always get exactly one response, within the request timeout (10 seconds by default; after that Takaro fails the call). Events go game → Takaro, are acknowledged and get no reply. There are no fire-and-forget requests: actions with nothing to return answer "payload": null.
Request args is a JSON-encoded string, not an object, in every version. An action without arguments gets "{}".
Handshake and version negotiation
Section titled “Handshake and version negotiation”The connection URL is unchanged (wss://connect.takaro.io/).
A version 1 identify (connector → Takaro):
{ "type": "identify", "payload": { "identityToken": "my-server-1", "registrationToken": "<registration token>", "name": "My Server", "game": "my-game", "connector": { "name": "my-game-connector", "version": "1.4.0" }, "protocolVersions": [1], "capabilities": [ { "name": "players.list" }, { "name": "players.get" }, { "name": "chat.send" }, { "name": "moderation.kick" }, { "name": "events.player-connected" }, { "name": "events.chat-message" }, { "name": "game.profile", "parameters": { "name": "My Game", "version": "1.0.2" } } ] }}identityToken(string, required) andregistrationToken(string, required) are unchanged from version 0.name(string, optional) is the server name shown in Takaro. Takaro trims it and cuts it to 50 characters. It must then be at least 3 characters long.- A new game server whose
nameis missing or too short is named after theidentityToken. - A game server created by a connector follows the
nameyou report on every identify, until an operator renames it in Takaro. From then on the operator’s name stays. - An invalid
namenever renames an existing game server.
- A new game server whose
game(string, optional) is the game identifier: a machine-readable slug for the game, such as7d2dorenshrouded. It matches^[a-z0-9][a-z0-9-]*$and is at most 64 characters. Absent ornullmeans the game is unknown. The dashboard shows it when the game declares no Game Profile.connector(object, optional) names the connector software and its release:{ "name", "version" }. Takaro records it per game server to see which connector releases are still in use.namefollows the same rule asgame.versionis a strict semantic version such as1.4.0or2.0.0-beta.1, at most 64 characters.- Absent or
nullis fine.
- A malformed
gameorconnectorfails identify withinvalid_args. Both fields are also accepted on version 0. protocolVersionsis an array of non-negative integers: the versions your connector implements. Takaro supports[0, 1]today and picks the highest version both sides know.- Absent or
null: version 0, as before. [1],[0, 1]or[1, 7]: version 1.[0]: version 0. You can opt into the version 1 handshake shape and still speak version 0.[]or[2, 3](no overlap): rejected withno_shared_protocol_version. An empty list is not the same as omitting the field.- Not an array of non-negative integers (for example
"1",[1.5]or[-1]): rejected withinvalid_args.
- Absent or
capabilitiesis the Capability Manifest (see below). On version 1, omitting it means an empty manifest, so every action excepttestReachabilityis refused withunsupported. On version 0 it is ignored.
Success (Takaro → connector):
{ "type": "identifyResponse", "payload": { "gameServerId": "6f1c2a4e-0b7d-4c1e-9a55-2f4f1a8e9d10", "protocolVersion": 1 }, "requestId": "a3d1c1f0-5b7e-4c55-8a3e-0c9e8f7d6b5a"}- Speak the returned
protocolVersionfor the rest of this connection. Version state is per connection, so negotiate again on every reconnect. - A version 0 connector now also receives
"protocolVersion": 0. Version 0 connectors ignore unknown fields, so nothing changes for them. - Older Takaro deployments ignore
protocolVersionsand send noprotocolVersion. Treat a missingprotocolVersionas0. A connector that also implements version 0 should offer[0, 1]and fall back. A version 1-only connector can offer[1].
Failure. Any identify that offered a valid protocolVersions list, and every negotiation failure, is answered like this:
{ "type": "identifyResponse", "payload": null, "error": { "code": "no_shared_protocol_version", "message": "No shared protocol version: Takaro supports [0, 1], connector offered [2, 3]", "details": { "takaroProtocolVersions": [0, 1], "connectorProtocolVersions": [2, 3] } }, "requestId": "b4e2d2a1-6c8f-4d66-9b4f-1d0a9f8e7c6b"}Takaro then closes the socket with close code 1008 (policy violation) and sets the close reason to the error code. The identify error codes are:
no_shared_protocol_version: no overlap. Only this code carriesdetailswith both lists. Tell the operator which side to update.invalid_args: a malformedprotocolVersions, a malformed manifest (including invalidgame.profileparameters), a malformedgameorconnector, a missingidentityTokenorregistrationToken, or a rejected registration token. Example messages:"No registrationToken provided", or for a manifest"invalid capability name: \"Players\"".internal: Takaro failed, for example"Internal error identifying game server"or"Could not identify game server". Retry with backoff.
no_shared_protocol_version is handshake-only. It is never a response error code.
Version 0 failures keep their old shape, { "type": "identifyResponse", "payload": { "error": { ... } } }, and the socket stays open, as before. See Adding support for a new game.
Requests, responses and errors
Section titled “Requests, responses and errors”A request (Takaro → connector) is unchanged from version 0:
{ "type": "request", "payload": { "action": "kickPlayer", "args": "{\"player\":{\"gameId\":\"12345\"},\"reason\":\"Spamming\"}" }, "requestId": "0d7c5b3e-8f2a-4b6c-9e1d-3a2b1c0f9e8d"}Success is the same as in version 0:
{ "type": "response", "requestId": "0d7c5b3e-8f2a-4b6c-9e1d-3a2b1c0f9e8d", "payload": null }Failure uses the version 1 standard error shape:
{ "type": "response", "requestId": "0d7c5b3e-8f2a-4b6c-9e1d-3a2b1c0f9e8d", "error": { "code": "not_found", "message": "Player 12345 is not online" }}The error codes:
| Code | Use it when |
|---|---|
unsupported |
The game cannot do this, for example an action you do not recognise. If your manifest is accurate you will rarely send it, because Takaro refuses undeclared capabilities before they reach you. |
invalid_args |
The arguments are malformed or out of range. |
not_found |
The thing the request names does not exist, such as a player who is not on the server, or a map tile that is not rendered yet. |
game_error |
The game itself failed to do it. |
internal |
Your connector failed unexpectedly. |
Rules:
- A present
errorwins over anypayload.error: null(or absent,false,"") means success, so a connector may always send the key. - An unknown
codebecomesinternal. A missing or emptymessagebecomes"Game server returned an error". A bare stringerrorbecomesinternalwith that string as the message. - The error reaches whoever called the action, such as a module or an API client, as a
GameProtocolErrorthat carries thecode. Module authors can tellnot_foundfrominvalid_argsfromgame_error. - Keep the existing null answers.
getPlayer,getPlayerLocationandgetPlayerProgressionstill answer"payload": nullfor an unknown player, as the reference page says, nevernot_found. Usenot_foundwhere an action has no null answer. - A successful payload is still validated against the action’s schema. If validation fails, Takaro fails the call and sends you
{ "type": "error", "payload": { "message": "..." }, "requestId": "<that request>" }so you can log it.
Capability Manifest
Section titled “Capability Manifest”A Capability is { "name": string, "parameters"?: object }. The manifest is the array of capabilities the game supports. Anything absent is unsupported. Do not declare an action you only answer with empty placeholder data.
On a version 1 connection, Takaro answers a request for an undeclared capability with unsupported itself, without sending it to you. You never need to handle requests you did not declare. The message looks like Game server did not declare capability moderation.ban (action banPlayer).
testReachability is always sent and is not a capability.
A version 0 connection has an implied manifest of the 15 action capabilities below (not map, not events.*). players.progression and locations.list (listLocations) are version 1 only and are not part of it. It can never declare anything else. Version 0 getMapInfo and getMapTile keep working on version 0 exactly as today.
Capability names
Section titled “Capability names”| Capability | Action(s) Takaro sends | Parameters |
|---|---|---|
players.list |
getPlayers |
none |
players.get |
getPlayer |
none |
players.location |
getPlayerLocation |
none |
players.inventory |
getPlayerInventory |
none |
players.progression |
getPlayerProgression (version 1 only) |
optional: { "source": "server" | "client-reported" }, default server. Declare client-reported when the data comes from a game client and could be spoofed. See getPlayerProgression |
players.teleport |
teleportPlayer |
none |
items.list |
listItems |
none |
items.give |
giveItem |
none |
entities.list |
listEntities |
none |
locations.list |
listLocations (version 1 only) |
none (also enables location markers on the map, grouped by category) |
chat.send |
sendMessage |
none |
console.execute |
executeConsoleCommand |
none |
moderation.kick |
kickPlayer |
none |
moderation.ban |
banPlayer |
none |
moderation.unban |
unbanPlayer |
none |
moderation.listBans |
listBans |
none |
server.shutdown |
shutdown |
none |
map |
getMapTile (version 1 shape), getMapImage |
required: { "dimensions": [...] }, see Map |
game.profile |
getGameIcon (only when an icon is declared) |
required: { "name", "version"?, "icon"? }, see Game Profile |
migration.native |
none (descriptive) | required: { "from": "<game identifier>" }. Promises the same player identities and item codes as Takaro’s built-in integration for that game, which a Connector Migration requires. from follows the same rule as the game identify field. |
events.player-positions |
none (you push player-positions) |
required: { "intervalSeconds": 1..60 }, see Pushed Positions. Turns off Position polling. |
events.inventory-changed |
none (you push inventory-changed) |
none. See Pushed inventory changes. Turns hourly snapshots on and diffing off. |
events.<type> |
none (descriptive) | none, for the other event types |
| (always) | testReachability |
not a capability |
| (version 0 only) | getMapInfo |
refused with unsupported on version 1; use map |
events.<type> entries for log, player-connected, player-disconnected, chat-message, player-death and entity-killed are descriptive. They filter nothing, so always send those events.
Two event Capabilities change Takaro’s behaviour. events.player-positions and events.inventory-changed are required before Takaro accepts those events, and declaring them changes what Takaro polls. Unknown event types are still rejected (Event delivery).
Validation rules
Section titled “Validation rules”A violation fails identify with invalid_args, or fails the capabilities message with invalid_args:
- The manifest is an array of at most 256 entries. Each entry is an object.
namematches^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9-]*)*$and is at most 128 characters. It starts with a lowercase letter and is made of dot-separated segments, and later segments may contain-.- No duplicate names.
parameters, if present, must be an object (not an array ornull).map,game.profile,migration.nativeandplayers.progressionparameters are validated strictly (see Map and Game Profile). An invalidplayers.progressionsourcerejects the whole manifest withinvalid_args. Unknown keys inside them are stripped. One invalid entry rejects the whole manifest.events.player-positionsparameters are validated strictly too. Unlike the others, any key other thanintervalSecondsrejects the manifest. The message isevents.player-positions requires parameters.intervalSeconds between 1 and 60.- Unknown but well-formed names are kept, with their parameters, so a connector can declare capabilities a future Takaro will understand.
Replacing the manifest
Section titled “Replacing the manifest”Send a capabilities message to replace the manifest mid-connection (version 1 only), for example when the map becomes available after the world loads, after a plugin reload, or when a map image changes revision:
{ "type": "capabilities", "requestId": "c0ffee00-1111-4222-8333-944455556666", "payload": { "capabilities": [ { "name": "players.list" }, { "name": "map", "parameters": { "dimensions": [ { "id": "overworld", "name": "Overworld", "axes": { "horizontal": { "field": "x", "direction": "right" }, "vertical": { "field": "z", "direction": "down" } }, "bounds": { "horizontal": { "min": -1000, "max": 1000 }, "vertical": { "min": -1000, "max": 1000 } }, "source": { "type": "tiled", "tileSize": 256, "minZoom": 0, "maxZoom": 5 } } ] } } ] }}- The message fully replaces the manifest. It does not merge. Send the complete list every time. An empty array is allowed and declares nothing.
- Acknowledgement:
{ "type": "capabilitiesResponse", "payload": null, "requestId": "<your requestId>" }. It is sent after Takaro has stored the manifest. If you omitrequestId, Takaro generates one. - Invalid:
{ "type": "capabilitiesResponse", "payload": null, "error": { "code": "invalid_args", "message": "..." }, "requestId": "..." }. The previous manifest stays in force and the socket stays open. - On a version 0 connection the message is ignored. There is no reply.
The manifest is stored per game server. Operators see it on the game server’s page in the dashboard. You can read it with GET /gameserver/{id}/capabilities (see the API reference). For a game server that declared nothing it reports the implied version 0 manifest with implied: true.
Event delivery
Section titled “Event delivery”Events are delivered at least once, on a best-effort basis. A version 1 event frame carries streamId and seq in payload, next to type and data, not inside data:
{ "type": "gameEvent", "payload": { "type": "chat-message", "data": { "player": { "gameId": "12345", "name": "Alice" }, "channel": "global", "msg": "hello" }, "streamId": "4f0e9a1c-6c2b-4d3a-9f8e-7b6a5c4d3e2f", "seq": 42 }}streamId: generate a random id once at process start. Keep it across reconnects and change it only when the process restarts. It must match^[A-Za-z0-9_-]{1,128}$(a UUID is fine).seq: 1, 2, 3, … per stream, strictly increasing, an integer of at least 1 and at most 2^53 − 1 (Number.MAX_SAFE_INTEGER, 9007199254740991). Send events inseqorder.- (
streamId,seq) is the deduplication key and the ordering tie-break for events with the same timestamp. Two distinct but byte-identical events are both kept on version 1, unlike version 0.
Deduplication is a high-water mark: Takaro remembers the highest seq handled per stream, and any event with seq at or below that mark is dropped as a duplicate (and acknowledged). This means:
- Never reuse a
streamIdwith aseqthat restarts at 1, or every event is dropped. A restart means a newstreamId. - Do not send lower
seqvalues after higher ones, except when resending. - Takaro remembers an idle stream for 7 days by default.
A missing or malformed streamId or seq gets an eventError and the event is not queued and not acknowledged. Fix the connector:
{ type: 'eventError', payload: { streamId: <valid value or null>, seq: <valid value or null> }, error: { code: 'invalid_args', message: 'Protocol version 1 events require streamId and seq' }}Acknowledgements
Section titled “Acknowledgements”Takaro acknowledges events cumulatively:
{ "type": "eventAck", "payload": { "streamId": "4f0e9a1c-6c2b-4d3a-9f8e-7b6a5c4d3e2f", "upTo": 42 }, "requestId": "d5f3e3b2-7d90-4e77-8c50-2e1b0a9f8d7c"}- It means every event of that stream with
seqat or belowupTois handled. Discard them from your buffer. - Takaro acks once events are queued for processing, not after the database write. Acks are batched: one ack after 100 handled events, or at most 1 second after the first unacknowledged one (server defaults, which may be tuned; do not depend on the exact numbers).
- Duplicates and rejected events are acknowledged too.
Rejected events
Section titled “Rejected events”Rejected events are acknowledged and reported, so you never resend a poison event forever:
{ "type": "eventError", "payload": { "streamId": "4f0e9a1c-6c2b-4d3a-9f8e-7b6a5c4d3e2f", "seq": 43 }, "error": { "code": "unsupported", "message": "Event blood-moon is not supported" }, "requestId": "e6a4f4c3-8ea1-4f88-9d61-3f2c1b0a9e8d"}unsupported: unknown eventtype. Custom events are out of scope. It also coversplayer-positionsorinventory-changedsent without declaring its Capability.invalid_args:datafailed validation. Unknown fields indataare rejected too. Aplayer-positionsbatch that arrives too soon is rejected the same way (Too-frequent batches).
If Takaro cannot queue an event, it sends eventError with code internal for that seq and closes the socket with close code 1011 (reason internal), without acking that event. Reconnect, identify, and resend from your oldest unacknowledged event.
What your connector must do
Section titled “What your connector must do”- Put every event in a bounded in-memory buffer before sending. Remove events when an
eventAckcovers them. - After every successful version 1
identifyResponse(including reconnects), resend every buffered event inseqorder, then continue with new events. - Size the buffer to hold at least 1,000 events. This is the recommended minimum and matches the Simulator default. Larger is better if memory allows.
- On overflow, drop the oldest event. Takaro sees the hole as a
seqgap. It logs gaps and counts them in a per-game-server metric. Gaps are not shown in the UI. - Never block the game thread on the network, the buffer or acks. Hand events to a background sender.
- Do not send events before
identifyResponse.
A reconnect with unacknowledged events looks like this:
connector Takaro | -- identify -------------------------> | | <------------------ identifyResponse - | | -- gameEvent seq 1, 2, 3 ------------> | | <------------------- eventAck upTo 3 - | | -- gameEvent seq 4 ------------------> | | -- gameEvent seq 5 --X (disconnect) | | -- identify -------------------------> | | <------------------ identifyResponse - | | -- gameEvent seq 4, 5 (resend) ------> | | <------------------- eventAck upTo 5 - |Game Profile
Section titled “Game Profile”A Game Profile is how a connected game presents itself in the dashboard: its display name, the game build it runs, and an icon. Takaro shows it next to the server across the dashboard, and the server overview lists the game build together with the connector and protocol versions.
The connector is the only source of a Game Profile. Takaro keeps no catalogue of known games, and operators cannot override the profile. They personalise a server through its name. Takaro stores the profile per game server, so two servers of the same game can show different profiles, for example while they run different connector releases.
Without a profile, the dashboard shows the game identifier from identify, and failing that “Generic”.
Declare it as the game.profile capability:
{ "name": "game.profile", "parameters": { "name": "Enshrouded", "version": "0.7.4.0", "icon": { "revision": "a1b2c3d4", "size": 18342 } }}| Field | Rule |
|---|---|
name |
Required. The game’s display name. 1 to 64 characters after trimming, without control, format (including bidi) or line and paragraph separator characters. |
version |
Optional. The game build. 1 to 32 printable ASCII characters. |
icon.revision |
Required when icon is present. 1 to 128 printable ASCII characters, no spaces. Opaque: a hash, timestamp or counter. Only equality matters. |
icon.size |
Required when icon is present. Exact byte length of the PNG, 1 to 131,072 (128 KiB). |
Invalid parameters reject the whole manifest with invalid_args (Validation rules). Unknown keys are stripped.
The game icon
Section titled “The game icon”The icon must be a PNG of exactly 256 × 256 pixels. Takaro re-encodes it for display.
Whenever Takaro sees an icon revision it has not stored yet (at identify, on every reconnect, or after a capabilities replacement), it fetches the icon in one request.
Request:
{ "type": "request", "payload": { "action": "getGameIcon", "args": "{}" }, "requestId": "5c2e7a10-3f4b-4d8e-a1c6-9b0d2e4f6a81"}Response:
{ type: 'response', requestId: '5c2e7a10-3f4b-4d8e-a1c6-9b0d2e4f6a81', payload: { revision: 'a1b2c3d4', data: '<base64 of the whole PNG>' }}getGameIconis never sent on version 0, and on version 1 only whengame.profileis declared with anicon.revisionin the response is the revision of the bytes you are serving.datais base64 of the whole file, exactlysizebytes. The icon is not chunked.
Takaro rejects the icon and remembers that revision when the decoded file is not size bytes, is not a PNG, is not 256 × 256, or cannot be decoded and re-encoded by Takaro. A rejected revision is never fetched again. To publish a fixed icon, declare a new revision.
A response revision that differs from the declared one is also a rejection, of the declared revision. The only exception is when you already sent a capabilities replacement with the new revision, in which case Takaro discards the fetch and fetches the new revision instead. So always declare a new revision before you serve its bytes, and keep serving the declared revision until then.
To change the name, version or icon, send a capabilities message with the full manifest. An unchanged revision is never fetched again, even across reconnects. If you drop the icon or the whole game.profile capability, Takaro removes the stored icon.
Icon changes, fetches and removals alike, apply only while the server is reachable. An unreachable server keeps its current icon, and Takaro catches up when it returns.
Answer game_error if the icon cannot be read (Takaro retries with backoff).
The dashboard renders a zoomable map per Dimension. The background comes from, in order: an image uploaded by the operator, then tiles rendered by the game (tiled), then one image exported by the game (image), then a plain coordinate grid. Player dots come from the Positions Takaro polls (getPlayerLocation, players.location). A game can instead push them with events.player-positions (Pushed Positions), and any player event can carry a Position (Position on player events). Locations from listLocations appear as markers when the game declares locations.list. Admins can filter them by category.
An example map capability with one Dimension per source type:
{ "name": "map", "parameters": { "dimensions": [ { "id": "overworld", "name": "Overworld", "axes": { "horizontal": { "field": "x", "direction": "right" }, "vertical": { "field": "z", "direction": "down" } }, "bounds": { "horizontal": { "min": -1000, "max": 1000 }, "vertical": { "min": -1000, "max": 1000 } }, "source": { "type": "tiled", "tileSize": 256, "minZoom": 0, "maxZoom": 5 } }, { "id": "nether", "name": "Nether", "axes": { "horizontal": { "field": "x", "direction": "right" }, "vertical": { "field": "z", "direction": "down" } }, "bounds": { "horizontal": { "min": -1000, "max": 1000 }, "vertical": { "min": -1000, "max": 1000 } }, "source": { "type": "image", "revision": "nether-1", "size": 1200000, "width": 1024, "height": 1024, "mimeType": "image/png" } }, { "id": "end", "name": "The End", "axes": { "horizontal": { "field": "x", "direction": "right" }, "vertical": { "field": "y", "direction": "up" } }, "bounds": { "horizontal": { "min": -1000, "max": 1000 }, "vertical": { "min": 0, "max": 512 } }, "source": { "type": "none" } } ] }}(The size above is illustrative. In reality it is the exact byte length of the encoded file.)
Dimensions, bounds and axes
Section titled “Dimensions, bounds and axes”| Field | Rules |
|---|---|
dimensions |
Array, 1 to 16 entries, ids unique. Declared order matters: the first is the default Dimension. |
id |
Matches ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$ (up to 64 characters, may contain _ . : -). Must equal the dimension value your positions report. |
name |
1 to 64 characters after trimming. Shown in the Dimension switcher. |
axes.horizontal |
{ "field": "x" | "y" | "z", "direction": "right" | "left" }: which position field runs left and right on screen, and which way its values increase. |
axes.vertical |
{ "field": "x" | "y" | "z", "direction": "up" | "down" }. Must use a different field than horizontal. |
bounds |
Optional for none, required for tiled and image. { "horizontal": { "min", "max" }, "vertical": { "min", "max" } } in raw game coordinates of the axis fields. Finite numbers, min lower than max. |
source |
{ "type": "none" }, a tiled source or an image source. Unknown keys are stripped. |
- A game like Minecraft or 7 Days to Die, with x and z and +z pointing south, uses horizontal
xrightand verticalzdown. A plain x/y game with +y pointing north uses horizontalxrightand verticalyup. - Stored positions stay in raw game coordinates. A position whose
dimensionis empty or absent belongs to the first declared Dimension. A position with adimensionthe game never declared is not drawn. - A manifest with an invalid
mapfails as a whole (invalid_args). The message is prefixedmap:, for examplemap: dimensions[0].bounds is required for a tiled source.
The none source
Section titled “The none source”Grid only. Use it for a Dimension the game cannot draw.
The tiled source
Section titled “The tiled source”{ "type": "tiled", "tileSize": 64..1024, "minZoom": 0..20, "maxZoom": 0..20 }, all integers, with minZoom at most maxZoom. Takaro asks for tiles on demand.
Request:
{ "type": "request", "payload": { "action": "getMapTile", "args": "{\"dimension\":\"overworld\",\"zoom\":1,\"x\":0,\"y\":0}" }, "requestId": "f7b5a5d4-9fb2-4a99-8e72-4a3d2c1b0f9e"}Response (the data is a 1x1 transparent PNG):
{ "type": "response", "requestId": "f7b5a5d4-9fb2-4a99-8e72-4a3d2c1b0f9e", "payload": { "mimeType": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABpfZFQAAAAABJRU5ErkJggg==" }}The tile layout is the standard web-map XYZ scheme:
- Project the bounds onto the screen with the axes.
- The frame is the square whose side is the longer of the two extents, anchored at the top-left corner of the projected bounds.
- Zoom 0 is one tile covering the frame. Zoom
zsplits it into2^zby2^ztiles. Tilexgrows to the right and tileygrows down. - Each tile is a
tileSizebytileSizeimage whose first pixel row is the top.
For the overworld above, the frame is x −1000..1000 by z −1000..1000. At zoom 1, tile (0,0) covers x −1000..0, z −1000..0 (the top-left quarter, because +z is down). Tile (1,1) covers x 0..1000, z 0..1000.
For non-square bounds, such as the End (x −1000..1000, y 0..512, y up), the frame is 2000 by 2000 and anchored at the top (y = 512). At zoom 0 the tile covers y 512 down to −1488. Where the frame extends beyond the bounds, answer not_found or a transparent image.
Takaro only asks for zoom within minZoom..maxZoom and 0 ≤ x, y < 2^zoom, and only for a declared Dimension with a tiled source. Out-of-range tiles are refused before they reach you. The arguments are always { dimension, zoom, x, y }. The legacy { x, y, z } shape is refused on version 1 with invalid_args.
Response rules:
mimeTypeis one ofimage/png,image/jpegorimage/webp.datais base64. Decoded it must be 1 byte to 1 MiB, and its magic bytes must matchmimeType, otherwise the tile is rejected.
If a tile is not rendered yet, answer the not_found error. Takaro shows a transparent tile and asks again after about a minute. Valid tiles are cached for about 10 minutes, so newly rendered areas appear within minutes without any message from you.
The image source
Section titled “The image source”For games that can export one full map image but cannot render tiles: { "type": "image", "revision": string, "size": int, "width": int, "height": int, "mimeType": string }.
| Field | Rule |
|---|---|
revision |
1 to 128 printable ASCII characters, no spaces. Opaque: a hash, timestamp or counter. Only equality matters. |
size |
Exact byte length of the encoded file, 1 to 33,554,432 (32 MiB). |
width, height |
Pixels, 1 to 8192 each. |
mimeType |
image/png, image/jpeg or image/webp. |
The image’s first pixel row is the top of the Dimension on screen and its first column is the left, after applying axes. The image is stretched over the bounds rectangle, so pixels need not be square in game units. Takaro cuts it into its own 256 px tile pyramid.
Whenever Takaro sees a revision it has not built yet (at identify, on every reconnect, or after a capabilities replacement), it fetches the image in sequential chunks.
Request:
{ "type": "request", "payload": { "action": "getMapImage", "args": "{\"dimension\":\"nether\",\"offset\":524288,\"length\":524288}" }, "requestId": "08c6b6e5-a0c3-4baa-9f83-5b4e3d2c1a0f"}Response:
{ type: 'response', requestId: '08c6b6e5-a0c3-4baa-9f83-5b4e3d2c1a0f', payload: { revision: 'nether-1', data: '<base64 of bytes offset..offset+length-1>' }}dimensionis a non-empty string,offsetan integer of at least 0, andlengthan integer from 1 to 1,048,576 (1 MiB). Takaro currently uses 512 KiB chunks, but honour anylengthin range.getMapImageis never sent on version 0, and on version 1 only whenmapis declared.revisionin the response is the revision of the bytes you are serving (1 to 128 characters).datais base64 of exactlymin(length, size − offset)bytes. Chunks are requested from offset 0 upwards, and the last chunk is shorter.
Takaro discards the fetch, stores nothing and retries later when:
- a chunk’s
revisiondiffers from the declared one (the image was re-exported mid-fetch), or - a chunk has the wrong length, or
- the assembled file’s real format, size, width or height differ from what was declared.
Serve the declared revision until you declare a new one. To publish a new image, send a capabilities message with the full manifest and the new revision. Takaro rebuilds the map. An unchanged revision is never fetched again, even across reconnects.
Answer game_error if the export fails (Takaro retries with backoff), and invalid_args for an offset past the end.
Operator upload
Section titled “Operator upload”Operators can upload a map image for any Dimension in the dashboard, setting bounds if the game declared none. An upload takes precedence over the game’s own source. This is the fallback for games that cannot provide any image, including version 0 connectors. Connectors need to do nothing for it.
The version 0 map
Section titled “The version 0 map”The version 0 getMapInfo and getMapTile { x, y, z } (7 Days to Die web-map layout) keep working on version 0 connections. On version 1, getMapInfo is refused and getMapTile uses the Dimension shape above. Declare map instead.
Pushed Positions
Section titled “Pushed Positions”By default Takaro polls getPlayerLocation for Positions. A game can push them instead. Declare events.player-positions with the interval at which you will send batches, then send one player-positions event per interval.
{ "name": "events.player-positions", "parameters": { "intervalSeconds": 5 } }intervalSeconds is required. It is a finite number from 1 to 60 inclusive, in seconds. Fractions are accepted.
parameters must be an object that holds exactly this one key. A missing key, any other key, or a value that is not a number or is out of range rejects the whole manifest with invalid_args. On identify the socket is closed with close code 1008. On a capabilities message you get a capabilitiesResponse error. The message is events.player-positions requires parameters.intervalSeconds between 1 and 60. This is stricter than map and game.profile, which strip unknown keys.
The player-positions event
Section titled “The player-positions event”Send one batch per interval, holding the Positions of every online player. It is a normal version 1 gameEvent with streamId and seq (Event delivery). payload.type is "player-positions".
{ "type": "gameEvent", "payload": { "type": "player-positions", "data": { "timestamp": "2026-10-22T12:00:05.000Z", "positions": [ { "gameId": "12345", "position": { "x": 123.45, "y": 67.89, "z": 10.11, "dimension": "overworld" } }, { "gameId": "67890", "position": { "x": -40.0, "y": 64.0, "z": 220.5 } } ] }, "streamId": "4f0e9a1c-6c2b-4d3a-9f8e-7b6a5c4d3e2f", "seq": 57 }}The fields of payload.data:
| Field | Type | Rule |
|---|---|---|
timestamp |
string, optional | ISO 8601. The time the Positions were observed. Defaults to the time Takaro received the batch. |
positions |
array, required | At most 1000 entries. May be empty. Every gameId must be unique in the batch. A duplicate is rejected with duplicate gameId <id> in player-positions batch. |
positions[].gameId |
string | 1 to 255 characters. |
positions[].position |
object, required | { "x": number, "y": number, "z": number, "dimension"?: string }. This is the same Position shape as getPlayerLocation. dimension follows the Map rules. |
Unknown fields anywhere in data are rejected with invalid_args.
All rejections are eventError frames. Like every rejected version 1 event, they are acknowledged, so do not resend them.
| Code | When |
|---|---|
unsupported |
The Capability was not declared on this connection: Game server did not declare capability events.player-positions. |
invalid_args |
data failed validation. The message holds the validation error. |
invalid_args |
The batch is too frequent. |
Too-frequent batches
Section titled “Too-frequent batches”Takaro limits the rate of batches so a connector cannot flood it. The rule is checked per connection, against the arrival time at Takaro, not against timestamp.
- A batch is rejected when it arrives less than
intervalSecondsminus a tolerance after the previous accepted batch. - The tolerance is the smaller of 10% of the interval and 1 second. With an interval of 5 s, batches 4.5 s apart are accepted.
- The first batch on a connection is always accepted.
- A rejected batch does not reset the clock.
- Batches resent after a reconnect go through the same gate. A burst of buffered batches is therefore mostly rejected, and that is harmless.
{ "type": "eventError", "payload": { "streamId": "4f0e9a1c-6c2b-4d3a-9f8e-7b6a5c4d3e2f", "seq": 58 }, "error": { "code": "invalid_args", "message": "player-positions batches must be at least 5 s apart (declared by events.player-positions); this one arrived 2130 ms after the previous accepted batch" }, "requestId": "e6a4f4c3-8ea1-4f88-9d61-3f2c1b0a9e8d"}What Takaro does with Positions
Section titled “What Takaro does with Positions”- A batch is not a Takaro event. It is never stored as an event, never appears in the events feed and never runs Hooks.
- A
gameIdthat Takaro does not know on this server is skipped silently. A batch never creates players and never changes online status. Online status still comes fromplayer-connected,player-disconnectedandgetPlayers. - It updates each player’s current Position and moves the player on the live map.
- It records the Positions into Tracking, thinned per player:
- at most one sample every 5 s;
- a sample is skipped when the player moved less than 0.5 game units in the same Dimension;
- a change of Dimension always counts as movement;
- at least one sample is kept every 60 s.
Polling
Section titled “Polling”- A server whose manifest declares
events.player-positionsis no longer askedgetPlayerLocationduring player sync. It is also not asked as a side effect of player events that carry no Position. - A server that does not declare it is polled exactly as before.
- Keep declaring
players.locationif you support it. Modules and API calls can still request it. - A
capabilitiesreplacement with a newintervalSecondstakes effect immediately. Takaro keeps the time of the last accepted batch. - Dropping the Capability makes later batches
unsupported. Takaro then polls Positions again.
Any player event can also carry a Position. See Position on player events.
Pushed inventory changes
Section titled “Pushed inventory changes”By default Takaro polls getPlayerInventory and works out changes by comparing snapshots. A game can push each change instead. Declare events.inventory-changed, which has no parameters, and send one inventory-changed event per changed item line.
{ "name": "events.inventory-changed" }The inventory-changed event
Section titled “The inventory-changed event”Send one event per changed item line. payload.type is "inventory-changed". This example is a player looting a container:
{ "type": "gameEvent", "payload": { "type": "inventory-changed", "data": { "player": { "gameId": "12345", "name": "Alice" }, "itemCode": "first_aid_bandage", "quantity": 5, "quality": "high", "cause": "loot", "position": { "x": 120.0, "y": 64.0, "z": 12.0, "dimension": "overworld" }, "counterpart": { "kind": "container", "containerId": "millbrook-chest", "position": { "x": 121.0, "y": 64.0, "z": 12.5, "dimension": "overworld" } } }, "streamId": "4f0e9a1c-6c2b-4d3a-9f8e-7b6a5c4d3e2f", "seq": 61 }}A trade loses an item and names the other player. Only data is shown:
{ "player": { "gameId": "12345", "name": "Alice" }, "itemCode": "weapon_baseball_bat", "quantity": -1, "cause": "trade", "position": { "x": 120.0, "y": 64.0, "z": 12.0 }, "counterpart": { "kind": "player", "gameId": "67890" }}A pickup gains an item and has no counterpart. Only data is shown:
{ "player": { "gameId": "12345", "name": "Alice" }, "itemCode": "first_aid_bandage", "quantity": 2, "cause": "pickup", "position": { "x": 118.5, "y": 64.0, "z": 10.0 }}The fields of payload.data:
| Field | Type | Rule |
|---|---|---|
timestamp |
string, optional | ISO 8601 time of the change. Defaults to the time Takaro received it. |
player |
object, required | The player object used by other events. gameId and name are required. Platform ids, ip and ping are optional. |
itemCode |
string, required | 1 to 255 characters. The same code as listItems and getPlayerInventory. |
quantity |
integer, required | Signed and never 0. Positive means gained, negative means lost. Bounded by ±1,000,000,000. |
quality |
string, optional | 0 to 255 characters. Same meaning as quality in getPlayerInventory. |
cause |
string, required | One of the causes. |
position |
object, required | The Position of the player at the change: { x, y, z, dimension? }. |
counterpart |
object, optional | A container or a player on the other side of the change. |
Unknown fields are rejected.
Rejections are eventError frames and are acknowledged:
unsupported:Game server did not declare capability events.inventory-changed.invalid_args:datafailed validation, including the counterpart rules below.
Causes
Section titled “Causes”The list is fixed. cause must be one of these:
| Cause | Meaning |
|---|---|
pickup |
Picked up from the world. |
drop |
Dropped into the world, or put into a container. A container counterpart goes with the container case. |
craft |
Created or used up by crafting. |
loot |
Taken from a container or from loot. A container counterpart goes with the container case. |
trade |
Exchanged with another player. A player counterpart goes with it. |
death |
Lost on death. |
other |
Anything else, such as an admin give. |
unknown is not accepted from a connector. Takaro reserves it for reconciliation.
Counterpart
Section titled “Counterpart”The optional counterpart names what was on the other side of the change. It has one of two shapes.
| Kind | Shape | Rules |
|---|---|---|
| Container | { "kind": "container", "containerId": string, "position": Position } |
containerId is 1 to 255 characters. containerId and position are both required. It must not carry gameId. |
| Player | { "kind": "player", "gameId": string } |
gameId is 1 to 255 characters. It must not carry containerId or position. |
A violation is rejected with invalid_args. Example messages:
counterpart.gameId is not allowed on a container counterpartcounterpart.containerId is not allowed on a player counterpartcounterpart.position is not allowed on a player counterpart
What Takaro does with inventory changes
Section titled “What Takaro does with inventory changes”- The change becomes Takaro’s existing
player-inventory-changedevent, so Hooks keep working. The event gains these optional fields:cause,positionandquantityChange(the signed quantity);counterpart, which is{ kind, containerId, position }or{ kind, gameId, playerId }.playerIdis filled when Takaro knows that player;changeType,previousQuantityandnewQuantity, which come from Takaro’s running view of the inventory.
msgreads likeItem gained: First Aid Bandage (first_aid_bandage) +5 (loot)orItem lost: Baseball Bat (weapon_baseball_bat) -1 (trade).- Takaro records every change in Tracking, but only for item codes it has already synced through
listItems. A change for an unsynced code is not recorded in Tracking. It still becomes an event, with the code used as the item name. - Rate limit: the event and Hook path is limited per game server.
- The defaults are 20 changes per 5 s and 120 per 30 s. These are server defaults and may change.
- Over the limit, the change is still recorded in Tracking but does not become a Takaro event or run Hooks.
- Takaro emits its usual rate-limited summary event.
- The connector is not told. The event is acknowledged normally.
Polling and reconciliation
Section titled “Polling and reconciliation”- A server that declares
events.inventory-changedis askedgetPlayerInventoryonly for an hourly snapshot per player. - Takaro no longer compares that server’s snapshots to create events, but it still needs the snapshots, so keep declaring
players.inventory. - Reconciliation: when the hourly snapshot disagrees with what the pushed changes add up to, the snapshot wins. Each item and quality that differs is recorded in Tracking as a change with cause
unknownand counted in a metric. A connector that misses changes becomes visible this way. - A server that does not declare the Capability is polled and compared as before.
Migrating a version 0 connector
Section titled “Migrating a version 0 connector”- Generate a
streamIdat process start. Keep anextSeqcounter starting at 1 (Event delivery). - In
identify, addprotocolVersions: [1](or[0, 1]if you keep version 0 support) andcapabilities(Handshake). - Build the manifest from what your connector really supports, using the capability names. If your game has a
capabilities.jsonor coverage table ingettakaro/connectors, declare exactly itslive-supportedactions, and leave outschema-fallbackandunsupportedones. Addevents.<type>for each event you emit. - Read
identifyResponse.payload.protocolVersionand treat a missing value as0. Branch your error and event handling on it per connection. - Handle identify failures:
error.codetogether with close code 1008. Do not reconnect in a tight loop onno_shared_protocol_versionorinvalid_args. Surface it to the operator. - Switch failures to
{ "type": "response", "requestId", "error": { "code", "message" } }with a code from the error table. Stop sendingtype: "error"frames and stringerrorfields. - Add
streamIdandseqto everygameEventpayload. Add the bounded buffer, resend after identify, drop-oldest, andeventAckandeventErrorhandling. Never block the game thread. - Optionally declare
map. ImplementgetMapTile { dimension, zoom, x, y }for a tiled source orgetMapImagefor an image source. Send acapabilitiesreplacement when the map becomes available or its image changes (Map). - Optionally send
gameandconnectorinidentify, and declaregame.profilewith agetGameIconhandler, so the dashboard shows your game’s name and icon instead of “Generic” (Game Profile). - Optionally declare
events.player-positionsand push oneplayer-positionsbatch per interval, declareevents.inventory-changedand push oneinventory-changedper item line, addpositionto player events, and addcategoryto Locations (Pushed Positions, Pushed inventory changes). - Verify against a Takaro instance. Check the game server’s capabilities in the dashboard or with
GET /gameserver/{id}/capabilities, call an undeclared action and expectunsupported, and restart the network mid-play and confirm no events are lost or duplicated.