Skip to main content

Telemetry API

For whoever maintains the service. If you're putting telemetry in a game, you want Adding it to a game instead. You don't need any of this.

Base URL: https://telemetry.ardenttitanite.com Auth: X-Telemetry-Key header.

Two keys, and they must differ:

KeyForCan
INGEST_KEYThe gamesWrite segments, register games
READ_KEYConsumersRead only

A leaked read key can't forge activity. That's the point of splitting them.

POST /v1/segments​

{
"eventId": "a-guid-generated-once-per-batch",
"gameKey": "genesius",
"placeId": "137231719757604",
"jobId": "server-job-id",
"serverTime": 1785735243003,
"segments": [
{
"robloxId": "1234567890",
"username": "Someone",
"team": "Blue",
"startedAt": 1785734043003,
"endedAt": 1785735243003,
"reason": "leave"
}
]
}
ResponseMeans
201 {duplicate: false}Stored
200 {duplicate: true}Already had this eventId. Nothing counted twice.
400Malformed. Retrying won't help.
403Game isn't registered, or is disabled.
401Wrong key.

Reuse eventId when retrying. That's the whole idempotency story. The sender never has to know whether its first attempt landed.

robloxId is a string. A Roblox ID above 2^53 rounds as a JSON number, and a rounded ID writes one player's session onto someone else's account.

POST /v1/games​

Register a game. Holding the ingest key isn't enough to report. A game has to be registered too, so one game can be switched off without rotating everyone's key.

{ "gameKey": "genesius", "label": "GENESIUS Territory", "placeId": "137231719757604" }

POST /v1/games/:gameKey/disable​

Stops that game reporting. Its existing records are untouched.

GET /v1/playtime​

?gameKey=genesius&since=<ms>&until=<ms>. Defaults to the last 7 days.

Computed from the raw rows on every request, excluded segments left out. Deliberately not a running total: a total that drifts can't be checked, whereas this can be recomputed and compared against the envelopes it came from.

GET /v1/stats​

Envelope, segment, excluded and game counts. Good for a health check.

GET /v1/presence​

?gameKey=<key>&since=<ms>&until=<ms>, who was in a game during a window.

This is what turns an event into an attendance sheet without anybody taking a register. It answers who was present, and nothing more. It does not decide who attended: the conditions for that live in Utilities, so the same telemetry can serve two groups that count attendance differently.

Windows longer than a day are refused rather than served slowly. An event window is hours, and a request for a year of a busy game is a mistake worth saying out loud.

A game that has since been disabled still answers here. Its old segments are real, and losing somebody's attendance because the game was switched off afterwards would be a poor trade.

GET /v1/players and /v1/players/:robloxId​

The player lookup behind the HQ console.

Read key only, never the ingest key. Every game holds the ingest key, and a game has no business asking what somebody did in a different game.

A 404 on one player means never recorded, which is a different answer from recorded with no playtime, and the two stay distinguishable.

GET /v1/teams​

The teams actually seen in the data, which is what the rule editor offers.

Teams are picked from this list rather than typed, because a rule matching a team name that no game ever reports is a rule that silently never fires.

The activity rules​

RouteDoes
GET /v1/rulesList them
POST /v1/rulesCreate one
PATCH /v1/rules/:idChange one
DELETE /v1/rules/:idRemove one
POST /v1/rules/:id/testFire one against current data without waiting

All five sit behind the read key, not the ingest key, and that split is the point. A rule can point a webhook at a Discord channel. Every game holds the ingest key, so a game that could write rules could make the service post into any server it liked.

The game gateway​

Three things a game already wanted, and previously could only have by holding a Utilities key of its own. Now telemetry holds the key and the game holds none.

RouteDoes
GET /v1/gateway/statusWill these work? Ask at start-up
POST /v1/gateway/pointsAward points in game
GET /v1/gateway/eventsIs an event running for my group right now
POST /v1/gateway/applicationsSubmit an application from in game

All four take the ingest key and require the game to be registered. Holding the key is not enough, which is what lets one game be switched off without rotating everybody else's.

status exists so a game can find out at start-up whether the capabilities it depends on will work, rather than discovering it when somebody is standing in a lobby waiting for points that were never going to arrive. It reports four things separately: the key you sent, whether the gateway is configured at all, whether your game is registered, and whether it is authorised.

Points keep their idempotency keys all the way through. Utilities refuses a repeated key, so a game retrying a forward it never saw the answer to does not pay twice. That is the same promise /v1/segments makes about eventId, kept by the same mechanism one service further along.

The events route does not take an attendance list. The game is told an event is running and nothing else. Presence is already arriving as segments, and asking the game to also report who attended would be asking the same question twice and then trusting the looser answer.

robloxUserId on an application is a string, for the reason robloxId is on a segment: an id above 2^53 rounds, and a rounded id files one person's application under somebody else's account.

When a segment gets flagged​

ReasonRule
ends before it startsendedAt earlier than startedAt
longer than the ceilingover MAX_SEGMENT_MS, default 12 hours
starts in the futurebeyond MAX_CLOCK_SKEW_MS, default 6 hours ahead
non-numeric timestampsnot a number

Flagged segments are stored, excluded from totals, and never deleted.

Running it​

cd ~/titan-telemetry
git pull && npm ci && npm run build
sudo systemctl restart titan-telemetry
sudo journalctl -u titan-telemetry -f

Port 8794 on 127.0.0.1, published by the cloudflared-titan-telemetry tunnel. Database at ~/titan-telemetry/data/telemetry.sqlite.