Ingest API reference

Reference

The HTTP endpoints the connector calls: pairing, sessions, heartbeats, in game events, confirmed bans, votes, heat, custom events, and batches.

The clip shows, in order:

  1. Every request carries the connector token.
  2. POST /api/v1/connector/pair redeems a code.
  3. POST /api/v1/telemetry/session per player.
  4. POST /api/v1/telemetry/heartbeat reports health.
  5. Heartbeats are what Health shows.

This is the surface the connector uses. It is documented so you can debug a connector, not so you can build a replacement, and it can change with a connector release.

#Authentication

Every telemetry request carries the connector credential as a bearer token. The token identifies the network and the server, so no request ever names a network id of its own.

The pairing endpoint is the exception: the code is the only credential it takes, and it is rate limited hard for that reason.

httpheaders.http
Authorization: Bearer mca_live_...
Content-Type: application/json

#POST /api/v1/connector/pair

Redeems a pairing code once and returns the credential for that server. An unknown code, an expired code, and an already used code all get the same 401, so the endpoint cannot be used to probe for valid codes.

jsonpair.json
// request
{ "code": "K3M9-QT4B" }

// 200 response
{
  "connectorToken": "mca_live_...",
  "networkId": "...",
  "serverId": "...",
  "serverName": "velocity-proxy",
  "platform": "velocity",
  "pairedAt": "2026-09-06T10:00:00.000Z"
}

#POST /api/v1/telemetry/session

Records one session and the player behind it in a single transaction. The connector sends it when the player joins, with no quitAt or durationSeconds, and again when they leave under the same sessionId, which completes that row. Answers 201 for a new session and 200 for a replay or a completion.

jsonsession-request.json
{
  "sessionId": "9b1f0c2a-4d3e-4a55-8f21-0c9b7e6a1234",
  "username": "Player123",
  "identityType": "premium_java",
  "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
  "hostname": "tiktok.myserver.net",
  "serverName": "velocity-proxy",
  "joinedAt": "2026-09-06T10:00:00.000Z",
  "authConfirmedAt": "2026-09-06T10:00:03.000Z",
  "quitAt": "2026-09-06T10:42:00.000Z",
  "durationSeconds": 2520,
  "authenticated": true
}

#POST /api/v1/telemetry/heartbeat

Reports the health of one server. The server it lands on comes from the credential, not from the body. tps and mspt may be null or left out: a Velocity proxy and a Folia server send neither.

The answer carries the network's switches in data: collectPlayerText, and features.lagProfiling, which is true only when the network owner turned lag profiling on and the plan is Network or up. A connector that has not heard an answer treats both as false. A batch that carries a heartbeat is answered with the same features.

jsonheartbeat-request.json
{
  "serverName": "velocity-proxy",
  "tps": 19.98,
  "mspt": 3.4,
  "memoryUsedMb": 2048,
  "memoryMaxMb": 8192,
  "onlineCount": 412,
  "recordedAt": "2026-09-06T10:00:00.000Z"
}

#POST /api/v1/telemetry/anticheat

Records one confirmed ban against a player the network already knows. isConfirmedBan must be true or nothing is stored.

jsonanticheat-request.json
{
  "eventId": "3f0a1b2c-5566-4a77-9b88-1c2d3e4f5a6b",
  "username": "Player123",
  "identityType": "premium_java",
  "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
  "serverName": "boxpvp-1",
  "checkName": "Simulation",
  "violationLevel": 42,
  "isConfirmedBan": true,
  "recordedAt": "2026-09-06T10:05:00.000Z"
}

#POST /api/v1/telemetry/lag-event

Records one sustained lag spike on a Paper server, with the Spark report link when the network owner turned lag profiling on. The answer is 403 while it is off or the plan is below Network, 201 the first time an event is seen, and 200 when a later send with the same clientEventId fills in endedAt or sparkUrl.

sparkUrl must look like https://spark.lucko.me/<id>. Anything else is stored as no link. A server may add ten events with a link in a rolling day, and the eleventh gets a 429. The body is strict: a field that is not listed is refused.

jsonlag-event-request.json
{
  "clientEventId": "0e7c1d52-9a3b-4f68-8c24-6a1b2d3e4f50",
  "serverName": "boxpvp-1",
  "startedAt": "2026-09-28T10:00:00.000Z",
  "endedAt": "2026-09-28T10:04:30.000Z",
  "trigger": "tps",
  "minTps": 14.2,
  "maxMspt": null,
  "playersOnline": 42,
  "sparkUrl": "https://spark.lucko.me/AbC123xYz"
}

#POST /api/v1/telemetry/vote

Records one vote that Votifier or NuVotifier received. uuid and bedrockXuid are what the connector resolved from a player the server knows, and are left out when it could not. A vote is matched to a player by those alone, never by username, and a vote that matches nobody is stored as unmatched and still answered 201.

The same site and name within 5 minutes, or a resend of the same body, is a replay and answered 200, so a vote NuVotifier forwards from a proxy to a backend counts once. Each network may record 300 votes a minute, and the next gets a 429 with Retry-After. The body is strict: a field that is not listed is refused, and votedAt may be up to 8 days old.

jsonvote-request.json
{
  "eventId": "5b0f2c1e-7d4a-4e8b-9c3f-1a2b3c4d5e6f",
  "serverName": "proxy-eu",
  "service": "MinecraftServers.org",
  "username": "Notch",
  "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
  "siteTimestamp": "1790000000",
  "votedAt": "2026-10-02T12:00:00.000Z"
}

#POST /api/v1/telemetry/heat

Records one minute of heat from a Paper server: up to 200 chunks, each with the looks that landed in it and the different players seen there today. A busy minute is split over several requests. regions carries the chunk bounds of WorldGuard regions and may be sent on its own.

eventId makes it safe to resend: the same id again is answered 200 and counted once. Below the Network plan a valid batch is answered 200 with skipped set and is not stored. The body is strict, and minute may be up to 8 days old.

jsonheat-request.json
{
  "eventId": "8c2d4f1a-3b5e-4c7d-9e1f-2a3b4c5d6e7f",
  "serverName": "survival",
  "minute": "2026-10-02T12:00:00.000Z",
  "intervalSeconds": 10,
  "cells": [
    { "world": "world", "x": 0, "z": -1, "samples": 42, "players": 17 },
    { "world": "world", "x": 15, "z": 0, "samples": 6, "players": 4 }
  ],
  "regions": [
    { "world": "world", "id": "spawn", "minX": -3, "minZ": -3, "maxX": 3, "maxZ": 3 }
  ]
}

#POST /api/v1/telemetry/custom-event

Records one event a plugin reported with MCAnalytics.track. The name is snake_case, props are flat text, numbers and true or false within 1 KB, and the player is named by uuid or bedrockXuid only. A resend with the same eventId is answered 200. Below the Network plan it answers 402 CUSTOM_EVENTS_NOT_IN_PLAN, which the connector drops without pausing anything else; a network past 50 names gets 422 EVENT_NAME_LIMIT. Sites and bots post the same shape to POST /api/v1/events with a key instead.

jsoncustom-event-request.json
{
  "eventId": "0f8e2a6c-1b3d-4c5e-9f70-8a9b0c1d2e3f",
  "serverName": "boxpvp-1",
  "name": "crate_opened",
  "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
  "props": { "crate": "legendary", "keys_left": 2, "from_vote": true },
  "at": "2026-10-02T12:00:00.000Z"
}

#POST /api/v1/telemetry/player-event

Records what one player did inside one session, up to 64 events per request, each with a kind, a time, a sequence number, and a small data object. The kinds are command, chat, death, kill, advancement, world_change, gamemode_change, teleport, respawn, kick, server_hop, client_info, region_enter, region_leave, quit_chunk, network_route, and the session summaries. The API keeps its own list and refuses any other kind.

Chat and command text is removed on arrival unless the network has switched it on, and the arguments of login and private message commands are removed either way.

jsonplayer-event-request.json
{
  "sessionId": "9b1f0c2a-4d3e-4a55-8f21-0c9b7e6a1234",
  "serverName": "boxpvp-1",
  "recordedAt": "2026-09-06T10:20:00.000Z",
  "events": [
    { "kind": "command", "at": "2026-09-06T10:05:12.000Z", "seq": 1, "data": { "label": "spawn" } },
    { "kind": "death", "at": "2026-09-06T10:11:40.000Z", "seq": 2, "data": { "cause": "lava" } }
  ]
}

#POST /api/v1/telemetry/batch

Carries up to 200 of the events above in one request, each with its type (session, heartbeat, anticheat, player_event, vote, heat, or custom_event) and its payload. The connector drains its spool through it. Every event goes through the same checks as its own route, and a rejected event is reported at its index while the rest still land, so the answer is 200 when all of them were taken and 207 when some were not.

#Errors and limits

Errors carry a code and a message. A 500 also carries an X-Error-Id header, which is the value to quote when you ask us about it.

Requests must arrive through the edge
The ingest API refuses anything that did not come through the trusted edge proxy, and rate limits before and after authentication.
StatusCodeMeaning
401UNAUTHORIZEDMissing, malformed, or revoked credential
403LAG_PROFILING_DISABLED, PLAN_REQUIREDLag events only: lag profiling is off, or the plan is below Network
409IDEMPOTENCY_CONFLICTSame id, different contents
422VALIDATION_ERRORThe body failed a field or range check
422EVENT_NAME_LIMITCustom events only: the network already uses 50 names
402CUSTOM_EVENTS_NOT_IN_PLANCustom events only: the plan is below Network
425PLAYER_NOT_RESOLVEDThe player has no session yet, retry shortly
429RATE_LIMITEDToo many requests, honour Retry-After
429LAG_LINK_LIMITLag events only: ten events with a Spark link in a day for this server