Skip to content

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.

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

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 "{}".

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) and registrationToken (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 name is missing or too short is named after the identityToken.
    • A game server created by a connector follows the name you report on every identify, until an operator renames it in Takaro. From then on the operator’s name stays.
    • An invalid name never renames an existing game server.
  • game (string, optional) is the game identifier: a machine-readable slug for the game, such as 7d2d or enshrouded. It matches ^[a-z0-9][a-z0-9-]*$ and is at most 64 characters. Absent or null means 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.
    • name follows the same rule as game.
    • version is a strict semantic version such as 1.4.0 or 2.0.0-beta.1, at most 64 characters.
    • Absent or null is fine.
  • A malformed game or connector fails identify with invalid_args. Both fields are also accepted on version 0.
  • protocolVersions is 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 with no_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 with invalid_args.
  • capabilities is the Capability Manifest (see below). On version 1, omitting it means an empty manifest, so every action except testReachability is refused with unsupported. 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 protocolVersion for 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 protocolVersions and send no protocolVersion. Treat a missing protocolVersion as 0. 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 carries details with both lists. Tell the operator which side to update.
  • invalid_args: a malformed protocolVersions, a malformed manifest (including invalid game.profile parameters), a malformed game or connector, a missing identityToken or registrationToken, 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.

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 error wins over any payload. error: null (or absent, false, "") means success, so a connector may always send the key.
  • An unknown code becomes internal. A missing or empty message becomes "Game server returned an error". A bare string error becomes internal with that string as the message.
  • The error reaches whoever called the action, such as a module or an API client, as a GameProtocolError that carries the code. Module authors can tell not_found from invalid_args from game_error.
  • Keep the existing null answers. getPlayer, getPlayerLocation and getPlayerProgression still answer "payload": null for an unknown player, as the reference page says, never not_found. Use not_found where 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.

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 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).

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.
  • name matches ^[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 or null).
  • map, game.profile, migration.native and players.progression parameters are validated strictly (see Map and Game Profile). An invalid players.progression source rejects the whole manifest with invalid_args. Unknown keys inside them are stripped. One invalid entry rejects the whole manifest.
  • events.player-positions parameters are validated strictly too. Unlike the others, any key other than intervalSeconds rejects the manifest. The message is events.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.

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 omit requestId, 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.

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 in seq order.
  • (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 streamId with a seq that restarts at 1, or every event is dropped. A restart means a new streamId.
  • Do not send lower seq values 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' }
}

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 seq at or below upTo is 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 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 event type. Custom events are out of scope. It also covers player-positions or inventory-changed sent without declaring its Capability.
  • invalid_args: data failed validation. Unknown fields in data are rejected too. A player-positions batch 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.

  1. Put every event in a bounded in-memory buffer before sending. Remove events when an eventAck covers them.
  2. After every successful version 1 identifyResponse (including reconnects), resend every buffered event in seq order, then continue with new events.
  3. 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.
  4. On overflow, drop the oldest event. Takaro sees the hole as a seq gap. It logs gaps and counts them in a per-game-server metric. Gaps are not shown in the UI.
  5. Never block the game thread on the network, the buffer or acks. Hand events to a background sender.
  6. 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 - |

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 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>' }
}
  • getGameIcon is never sent on version 0, and on version 1 only when game.profile is declared with an icon.
  • revision in the response is the revision of the bytes you are serving.
  • data is base64 of the whole file, exactly size bytes. 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.)

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 x right and vertical z down. A plain x/y game with +y pointing north uses horizontal x right and vertical y up.
  • Stored positions stay in raw game coordinates. A position whose dimension is empty or absent belongs to the first declared Dimension. A position with a dimension the game never declared is not drawn.
  • A manifest with an invalid map fails as a whole (invalid_args). The message is prefixed map: , for example map: dimensions[0].bounds is required for a tiled source.

Grid only. Use it for a Dimension the game cannot draw.

{ "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:

  1. Project the bounds onto the screen with the axes.
  2. The frame is the square whose side is the longer of the two extents, anchored at the top-left corner of the projected bounds.
  3. Zoom 0 is one tile covering the frame. Zoom z splits it into 2^z by 2^z tiles. Tile x grows to the right and tile y grows down.
  4. Each tile is a tileSize by tileSize image 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:

  • mimeType is one of image/png, image/jpeg or image/webp.
  • data is base64. Decoded it must be 1 byte to 1 MiB, and its magic bytes must match mimeType, 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.

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>' }
}
  • dimension is a non-empty string, offset an integer of at least 0, and length an integer from 1 to 1,048,576 (1 MiB). Takaro currently uses 512 KiB chunks, but honour any length in range. getMapImage is never sent on version 0, and on version 1 only when map is declared.
  • revision in the response is the revision of the bytes you are serving (1 to 128 characters).
  • data is base64 of exactly min(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 revision differs 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.

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 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.

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.

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.

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 intervalSeconds minus 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"
}
  • 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 gameId that Takaro does not know on this server is skipped silently. A batch never creates players and never changes online status. Online status still comes from player-connected, player-disconnected and getPlayers.
  • 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.
  • A server whose manifest declares events.player-positions is no longer asked getPlayerLocation during 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.location if you support it. Modules and API calls can still request it.
  • A capabilities replacement with a new intervalSeconds takes 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.

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" }

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: data failed validation, including the counterpart rules below.

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.

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 counterpart
  • counterpart.containerId is not allowed on a player counterpart
  • counterpart.position is not allowed on a player counterpart
  • The change becomes Takaro’s existing player-inventory-changed event, so Hooks keep working. The event gains these optional fields:
    • cause, position and quantityChange (the signed quantity);
    • counterpart, which is { kind, containerId, position } or { kind, gameId, playerId }. playerId is filled when Takaro knows that player;
    • changeType, previousQuantity and newQuantity, which come from Takaro’s running view of the inventory.
  • msg reads like Item gained: First Aid Bandage (first_aid_bandage) +5 (loot) or Item 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.
  • A server that declares events.inventory-changed is asked getPlayerInventory only 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 unknown and 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.
  1. Generate a streamId at process start. Keep a nextSeq counter starting at 1 (Event delivery).
  2. In identify, add protocolVersions: [1] (or [0, 1] if you keep version 0 support) and capabilities (Handshake).
  3. Build the manifest from what your connector really supports, using the capability names. If your game has a capabilities.json or coverage table in gettakaro/connectors, declare exactly its live-supported actions, and leave out schema-fallback and unsupported ones. Add events.<type> for each event you emit.
  4. Read identifyResponse.payload.protocolVersion and treat a missing value as 0. Branch your error and event handling on it per connection.
  5. Handle identify failures: error.code together with close code 1008. Do not reconnect in a tight loop on no_shared_protocol_version or invalid_args. Surface it to the operator.
  6. Switch failures to { "type": "response", "requestId", "error": { "code", "message" } } with a code from the error table. Stop sending type: "error" frames and string error fields.
  7. Add streamId and seq to every gameEvent payload. Add the bounded buffer, resend after identify, drop-oldest, and eventAck and eventError handling. Never block the game thread.
  8. Optionally declare map. Implement getMapTile { dimension, zoom, x, y } for a tiled source or getMapImage for an image source. Send a capabilities replacement when the map becomes available or its image changes (Map).
  9. Optionally send game and connector in identify, and declare game.profile with a getGameIcon handler, so the dashboard shows your game’s name and icon instead of “Generic” (Game Profile).
  10. Optionally declare events.player-positions and push one player-positions batch per interval, declare events.inventory-changed and push one inventory-changed per item line, add position to player events, and add category to Locations (Pushed Positions, Pushed inventory changes).
  11. 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 expect unsupported, and restart the network mid-play and confirm no events are lost or duplicated.