Quickstart
Sign up to create your studio, add a game to mint a per-game SDK token (tmb_…), then initialize the SDK once. Managed exceptions and session heartbeats are captured automatically. Players are never anonymous: the SDK mints a persistent salted device id (dev_…) and, once SetUser runs, the session's pre-auth data merges into the real player id.
Core concepts
Four primitives power everything in Tombstack:
Install & init
Add the UPM package com.anklebreaker.tombstack in Unity 6 (6000.0)+ — download the .tgz from the download page and use Window ▸ Package Manager ▸ + ▸ Add package from tarball…:
Grab the latest .tgz from the download page (it always serves the current release).
Prefer git? Add package from git URL… works for everyone — the package lives in a public repository: https://github.com/AnkleBreaker-Studio/tombstack-unity.git#v0.19.6. Zero-code init: create a Tombstack ▸ Config asset under a Resources/ folder named TombstackConfig — it auto-initializes on load.
TombstackConfig asset ships placeholders — Endpoint is https://your-tenant.example.com and Game Token is tmb_REPLACE_ME. Set Endpoint to https://tombstack.com and paste a per-game SDK token (Game ▸ SDK tokens ▸ Mint token — shown once). The placeholder host does not resolve, so the SDK refuses to initialise while either field is unfilled and logs a single console error saying so — it will not fail quietly.What's automatic
After Tombstack.Init (or zero-code auto-init), the SDK is fully autonomous — no try/catch wiring, no log-shipping code:
Every crash report carries an optional kind so the dashboard stops calling every report a "crash" — the Graveyard has a Kind filter and labels each crash distinctly:
The field is optional on the wire — older SDKs omit it and the server derives the kind, so it's fully back-compatible.
Retain more or fewer launch logs via TombstackConfig ▸ Retained Launch Logs (or Init(…, retainedLaunchLogs: N), 1–10; default 3).
Logs surface in the dashboard as Player log download links on the signature detail and bug detail pages, and GDPR erasure deletes them with the player's other artifacts. All three systems are config toggles (default ON) and consent-gated: with Require Consent, nothing is captured until Tombstack.SetConsent(true).
Capturing crashes
Managed C# exceptions and session heartbeats upload automatically; failed uploads persist and retry on next launch. Capture handled exceptions explicitly to keep context.
Session heartbeats fire every 60 s (they drive live CCU + the crash-rate denominator) and carry frame stats — including a per-~20 s fpsSamples series folded into the beat, so the dashboard sees sub-beat FPS without extra rows. The SDK also sends an on-demand beat when the app is backgrounded (minimized) and, best-effort, on quit, so liveness stays tight. Force one at any lifecycle moment with Tombstack.SendHeartbeatNow() — gated exactly like the periodic loop (consent + heartbeats enabled + session started) and de-duped server-side, so it never double-counts CCU.
Player bug reports
Let players file bug reports from inside the game — optionally with a screenshot. They land in the dashboard alongside crashes.
Analytics events
Track gameplay analytics with one-liners. Events accept flat string attributes (≤32 keys) that power the event browser, per-key rollups, player timelines and funnel conditions; metrics get time-series with p50/p95/p99. Both are batched client-side (flush on count ≥ 50 / age ≥ 60 s / pause / quit) and calls made before init are buffered (64, drop-oldest) and replayed with their original timestamps once the SDK initializes.
Typed helpers (SDK 0.10.0+) emit standard tmb.* events — plain custom events underneath, so batching, funnels and attribute filters all apply — that the dashboard auto-recognizes: tmb.progression events render an automatic level-progression table (starts / completes / fails / fail:complete / completion %) on the Analytics page, with TrackEconomy, TrackPurchase and TrackAdImpression covering economy, IAP and ads.
Environments
Every payload carries an environment label (default production) so one game can hold production / staging / development builds without mixing data. Set it zero-code via the Environment field on the TombstackConfig asset, or in code — an explicit SetEnvironment always wins over the config value, even when called before init. Environments are free-form (≤64 chars) and self-registering — the first heartbeat from a new label makes it appear in the dashboard's environment selector. You can also create, rename, or delete labels from the dashboard before any build reports. Every page, CSV export, the read API (?environment=) and the MCP tools filter by it.
Native C / C++ (Unreal, Godot, custom)
Any engine that can call C can use Tombstack through the native SDK v0.9.1 — a pure C99 ABI with the same wire protocol as the Unity SDK. That includes Unreal today, via the C ABI; a drop-in .uplugin is on the roadmap, not shipped. There is no prebuilt binary: you build from source, which is one CMake invocation on Windows, Linux or macOS.
Note the symbol prefix is tombstone_ and the library is tombstone. That is deliberate and stable: the product was renamed to Tombstack, the ABI was not. A snippet spelling tombstack_*, or including tombstack/tombstack.h, is wrong — that path does not exist.
endpoint, token and build_version are the three required fields; everything else has a default. Call tombstone_flush(timeout_ms) before exit and tombstone_shutdown() on a clean quit. Every entry point returns a tombstone_result and never throws.
Full reference — including the pre-init buffering rules, the opt-in POSIX/ELF crash handler (enable_native_crash_handler, default off) and the offline sidecar queue — is the header itself: AnkleBreaker-Studio/tombstack-native.
Authentication
All endpoints authenticate with a token sent as Authorization: Bearer … (or x-api-key). Two kinds exist:
The public read and triage APIs require a studio key (tmb_st_…) or a per-game token that was granted the read/write scope. A default ingest-only SDK token is rejected with 403; a missing or unknown token gets 401.
/api/v1/studio/gamesStudio key only — list the studio's games (for fan-out)Every response uses the envelope { "success": true, "data": … } / { "success": false, "error": "…" }. Common error codes: 400 validation, 401 bad/revoked token, 404 not found, 429 rate limited (with Retry-After).
A validation failure names the field. Every ingest 400 carries an issues array beside error, one entry per rejected field: { "field": "os", "message": "Invalid enum value. Expected 'windows' | … , received 'solaris'", "code": "invalid_enum_value" }. On a *:batch route each entry also carries item — the 0-based index into the items array you sent — so you never have to bisect a 200-item envelope to find the bad row. The list is capped at 20 entries; when more were found, issuesOmitted gives the remainder as a count, and it is absent when nothing was omitted. error keeps its exact previous wording, so a client already branching on it is unaffected.
Rate limits (fixed 1-minute window). These are the ingestion limits and do not apply to the read API — see the note below, which an earlier version of this page omitted, so an integrator sizing a poller against 20,000/min would have started getting 429s at 600. The bound to size against is 20,000 requests/min per game key, deliberately high so a real crash spike across your whole player base is never throttled.
There is a second, much higher ceiling of 12,000 requests/min per network origin — a runaway backstop, not a per-machine guard. This page previously described it as 120/min per IP, “the abuse guard for a crash-looping client”. That was wrong in both halves: the limiter keyed on a request header the caller writes, so it bounded nobody, and now that it keys on a value the caller cannot write, that value identifies a network edge rather than one machine. We would rather publish the ceiling we actually enforce than a per-machine promise we cannot keep at this layer.
The read API (/api/v1/read/*) is bounded far lower, because each call is an aggregate query rather than a single row write: 600 requests/min per key, same 1-minute window, with a 3,000/min per network origin backstop above it. Size dashboards and CI pollers against the per-key number, not the ingestion one. A per-origin gate is also applied before your key is verified, so a burst of invalid keys is rejected without touching the database.
A separate editor API (/api/editor/*) powers the Unity plugin's in-editor hub. It is plugin-internal: your game never calls it, it is authenticated with a per-developer editor token rather than an SDK token, and it is not part of the versioned /api/v1 contract below — it may change with any plugin release. Nothing here depends on it. (This paragraph previously pointed at a file in a private repository, which no customer can open.)
Ingest API
Game clients are treated as hostile input: every field is validated, clamped and rate-limited. os ∈ {windows,macos,linux,android,ios,other}, arch ∈ {x64,arm64,x86,other}, timestamps within the 90-day window.
/api/v1/ingest/crashes201 { crashId, minidumpUpload?, screenshotUpload?, logUpload? }/api/v1/ingest/heartbeats202 { accepted: true, pendingRequests[] } — pendingRequests is the log-pull channel, [] on almost every beat/api/v1/ingest/bug-reports201 { bugId, screenshotUpload?, logUpload? }/api/v1/ingest/events202 { eventId } — or 202 { stored: false, budget } when the session's custom-row budget (60 per 30 min) is spent/api/v1/ingest/metrics202 { metricId } — single numeric metric; 202 { stored: false, budget } when the session budget is spent/api/v1/ingest/events:batch202 { accepted, dropped, skewMs, driftAppliedMs, clamped }/api/v1/ingest/metrics:batch202 { accepted, dropped, skewMs, driftAppliedMs, clamped }High-frequency telemetry batches via { sentAtIso, items: [...] } (1–200 items, ≤512 KB). Each item is validated independently — a bad item is dropped and the rest stored — and carries its own occurredAtIso; the envelope's sentAtIso only computes clock skew. A batch is charged its item count against the rate window, so batching can't amplify ingest past the per-key limit.
Events accept an optional attributes map (≤32 entries; keys ≤64 chars; values string ≤512 chars, number, or boolean). Attributes surface in the dashboard's event browser, per-key rollups, and funnel conditions, and are returned by GET /api/v1/read/events.
Binary artifacts upload out-of-band via presigned S3 multipart POSTs (15-min TTL) requested with boolean flags: crashes accept "minidump": true (response carries data.minidumpUpload) and bug reports accept "screenshot": true (data.screenshotUpload). Both accept "log": true to request a player-log slot (data.logUpload, text/plain). Read APIs return a short-lived logUrl wherever a log exists.
Crashes accept an optional kind ∈ crash | exception | unclean_shutdown so the dashboard labels each report accurately (and the Graveyard can filter by it). Omit it and the server derives the kind — fully back-compatible.
crashId is a 26-character opaque id — today a lowercase hex digest, derived from the crash's natural key so a client retry collapses onto the same row instead of duplicating it. Do not pattern-match it: rows written before that scheme carry a 26-character ULID instead, so both shapes are in circulation and neither is a promise.
The three upload keys are absent, not null, unless you asked for them — test with "logUpload" in data, never === null. Request one and you get the presigned slot back:
fields and formFields carry the same presigned-POST policy in two encodings — a map for browser consumers, and an ordered array of {k,v} pairs for Unity, whose JsonUtility cannot deserialize the hyphenated AWS keys. Send every entry, in order, before the file part.
One more field appears when — and only when — it applies: clockSkewMs, present on the five single-item routes above when the device's clock was far enough off that we had to move the timestamp we stored. A healthy client never sees it. The two :batch routes report the same condition differently and do not return this field — they return skewMs, driftAppliedMs (the envelope-wide correction in ms) and clamped, which is a count of items pinned to a window edge, not a flag. It is all there so a wrong clock shows up as a number instead of as telemetry that quietly landed on the wrong day.
Read API
Pull aggregated and recent data back out — for dashboards, automations or your own tooling.
crashes, bug-reports, signatures, events and players/{userId}/crashes; 2,000 for a single match's timeline; 50 for a server's boot sessions. The slice is taken after a newest-first sort, so you get the most recent N, never an arbitrary N — and with ?days= the window is always [now − days, now], so shrinking days does not page backwards, it returns a subset of what you already had. Pass an absolute window instead — ?from= and ?to=, ISO-8601 UTC — to name a fixed historical interval that reads the same population tomorrow as it does today. To reach past that, add ?pageSize= to crashes, bug-reports or events and follow nextCursor until it is null — that walk pins its window and returns every matching row in it, which is the historical backfill this API used to have no endpoint for. signatures stays unpaged on purpose (it is a group-by: pages would carry partial groups, and affectedUsers is a distinct count that does not sum — page crashes and group client-side). Two other read routes have their own older pagination: live sessions, via offset/limit, and a server's boot sessions, via ?page=.Cursor paging, in five rules. One: stop on nextCursor: null, never on a row count — hasMore is derived from it, a page holding exactly pageSize rows may be the last or the middle, and a filter can leave a page with four rows while thousands remain behind it. Two: the window is pinned when you ask for page 1, with its end floored to the hour, so the export is a snapshot rather than a moving target; window.lagMs publishes exactly how far behind your clock that edge sits, and rows newer than window.endIso belong to the next walk. Three: the cursor is opaque and signed — it carries no database key material, and a mangled or hand-edited one is a 400, never a silent restart at the newest row (that restart is an infinite loop that returns 200 OK every time). It is bound to the query that minted it, so changing days, pageSize or any filter mid-walk is a 400 naming which; following the Link: <…>; rel="next" header carries them all forward for you. Four: count appears only on the final page and is then the exact total for the pinned window; before it you get countAtLeast, a floor that grows page by page. Five: a paged events call can also use userId. It walks that one player on a separately-signed cursor, because the flat refusal that used to sit here left a real gap: the unpaged userId slice is also 200 rows newest-first, so a player with more than 200 events in the window was readable only from the end of their history, never their first day. One caveat, published in the response rather than implied: their pre-sign-in rows live under device-derived dev_… ids in separate index partitions and one cursor names one partition, so aliasFanIn.userIds lists them for you to page the same way, and until that list is empty the total is countAtLeast, never count.
Nothing changes for a request that sends neither pageSize nor cursor: it gets byte-identical responses to the ones it got before paging existed — same 200-row slice, same fields, same truncated semantics.
What you do get is an honest account of what is missing, in three fields that are easy to conflate. returned is the size of the slice you were handed — not a statement about the read. count is the full matched total, and it is present only when the read completed; when the read stopped early it is replaced by countAtLeast, a floor rather than a total. truncated says the store had more rows to give, and it is derived from a leftover DynamoDB cursor rather than from rows.length >= cap — a filtered read can match a few dozen rows, spend its page budget and still be holding a cursor. stoppedBy tells "narrow the window" (row-budget / page-budget) from "retry" (time-budget).
returned < count means there is more data in the window you cannot fetch; truncated: true means there is more in the store the read never reached. Neither implies the other — a complete read of 5,000 crashes reports truncated: false with returned: 200, so code that paginates on truncated loops forever while code that compares returned to count at least knows its answer is partial. On signatures there is a third, independent truncation: signatureCapReached. Note its signatureCap is 1,000 — the grouping cap, a different number from the 200 rows you receive — so signatureCapReached: false does not mean you got every signature.
Two routes deviate from that contract, and both deviations are live today. events does not demote count to countAtLeast on a truncated read — it publishes count beside truncated: true, so on that one route count is a floor whenever truncated is true; read coveredFromIso, the oldest row actually returned, for the window you really got rather than the one you asked for. And players/{userId}/crashes caps at 200 while publishing no truncation flag for that cap (devicesTruncated covers only the multi-device identity fan-in), and its count equals the number of rows returned — so a player with 250 crashes reports count: 200 with nothing marking the shortfall. Read that 200 as "at least 200".
/api/v1/read/crashes/summary?days=30Aggregated — no row cap, this one is already folded/api/v1/read/crashes?days=7Recent rows — newest ≤200, read count + truncated/api/v1/read/crashes?days=30&pageSize=500&cursor=…PAGED export — pinned window, walk nextCursor to null for every crash in it/api/v1/read/signatures?days=30Crash signatures + triage status — ≤200 rows, regressions first then by count. NOT all of them, and NOT pageable (a group-by)/api/v1/read/signatures/{signature}Drill-down + trend + logUrl/api/v1/read/signatures/{signature}/analyzeOne-call analyze bundle for AI triage — stack, trend, tags, affected players, breadcrumbs, recent occurrences, duplicate siblings with a merge verdict/api/v1/read/bug-reports?days=30Recent reports — newest ≤200, read count + truncated. Add pageSize/cursor to page it/api/v1/read/bug-reports/{bugId}?at=…One report + screenshot/logUrl/api/v1/read/events?days=7&name=level_complete&userId=user-123Recent events incl. attributes — newest ≤200. name filters one event, userId scopes to one player. pageSize/cursor page it, userId included/api/v1/read/players/{userId}/crashesOne player's crashes — ≤200, with count / countAtLeast + truncated like the other read routes/api/v1/read/usageCCU peak, plan, est. USD/api/v1/read/matches?days=7Derived matches — span, players, server, crash count/api/v1/read/matches/{matchId}One match's full telemetry timeline/api/v1/read/metrics?name=fps&days=7Series + percentiles (groupBy serverId|matchId|buildVersion|os)/api/v1/read/retention?days=30DAU/WAU/MAU + D1/D7/D30 cohort (player rows only; format=csv supported)/api/v1/read/retention?by=osSegmented cohorts — by os|build_version|platform_class|device_model|gpu_vendor|gpu|ram_band|vram_band|cpu_cores|engine|country (format=csv → one row per segment)/api/v1/read/releases?days=30Per-build release health WITH the verdict — crashFree.quotable / verdictText per build, grading.unjudged for the fleet; never rank builds by raw crash counts/api/v1/read/churn?days=30Where players quit — churn rate (null when the scan truncated), crashed-on-exit vs stayers, capped exit points with their basis/api/v1/read/audiencesAudience definitions — standard ids + saved (id, name, rules)/api/v1/read/audiences/members?audienceId=…One audience's member userIds, with a coverage block saying how much of the window was read/api/v1/read/servers?days=7Fleet list — live CCU, GRADED crash-free (crashFree.quotable / verdictText, never a bare rate), last seen/api/v1/read/servers/{serverId}Server detail — metadata, health, recent crashes/api/v1/read/servers/{serverId}/connected?days=1Players connected to the server in the window — one players[] entry per (player, match) pair/api/v1/read/servers/{serverId}/sessions?days=7&page=1Server boot sessions — 50/page, PAGINATED via ?page= (page, pageCount, total, hasMore)/api/v1/read/pull-requestsLog-pull status + audit trailLive sessions — the dashboard's "Live now" panel pages through currently-live player sessions (last beat inside the ~10-minute live window). It is session-cookie authed (any studio member), not a token route:
/api/v1/read/games/{gameId}/live-sessions?environment=&platform=&offset=&limit=Paginated live sessions (last beat within the 5-minute session window) — offset (≥0) / limit (1–100, def 25) + env/platform filtersEach row is a PlayerSession plus historyClipped — true when the session's first observed beat sits at the edge of the history window, i.e. it began before we looked. For those rows the beat count and duration are minima, not measurements(the bug this field exists for showed a one-hour, 58-beat session as "14 beats"). total is the size of the full live set before the slice, so page with it rather than with sessions.length.
Retention cohorts can also be split by ?by=country — the viewer country is server-derived at the edge (CloudFront viewer-country header, with a GeoIP fallback on the client IP), never sent by the SDK, so per-country cohorts work with no game-side field. Unknown IPs fall into an (unknown) bucket.
The five list/summary routes (crashes, crashes/summary, signatures, bug-reports, events) accept an optional ?environment= filter — production also matches legacy rows written before the environment feature, all matches everything, and omitting the param returns all environments.
Six read routes (crashes, crashes/summary, events, metrics, bug-reports, retention) also accept an optional ?platform= filter over the rows' OS signal. Valid values: all | mobile | desktop | windows | macos | linux | android | ios | other (mobile = android+ios, desktop = windows+macos+linux). Any other value is a 400 "invalid platform" — never silently ignored. crashes/summary scopes both its crash numerator and its session denominator. The filter never touches live CCU, adoption denominators or the billed monthly CCU peak — your bill never changes with it.
Fleet & log-pull writes (write scope, except fulfill which is ingest): enrich a server's metadata, raise a player-log pull, and let a targeted client honour it (uploading only its own log). The server registry row is created lazily by role=server telemetry, so POST /servers only enriches an existing server (unknown serverId → 404).
/api/v1/serversBody { serverId, region?, capacity?, hostname?, build?, status? }/api/v1/pull-requestsBody { targetType, targetValue, reason, ttlSeconds? } → 201 { requestId }/api/v1/pull-requests/{requestId}/fulfillBody { sessionId, nonce, nonceExpiry, nonceSessionId?, userId?, matchId?, serverId? } — nonce from the heartbeat ack → 201 { logUpload }Triage writes back over the same token auth — set { "status": "open" | "resolved" | "ignored" } on a signature or a single bug report:
/api/v1/signatures/{signature}/statusBody { status, note? }/api/v1/bug-reports/{bugId}/status?at=…Body { status }Abridged deliberately — the real crashes/summary body carries about twenty fields, and three of them decide whether the rest mean anything. crashRate24h / crashRate7d are per-mille and are null, never 0, when the window has no session denominator — render "—", because "0.0/1k" reads as a flawless game you in fact know nothing about. rateSessions24h / rateSessions7d publish the exact divisor each rate used so you can reproduce the quotient, and rateBasisExact says whether that divisor is a real count or a prorated estimate — round an estimate into an integer and you have fabricated a number. topCrashSignatures is a 7-day list capped at 20 for display, so never render its length as a quantity; signatureCount24h is the uncapped 24-hour population, and byBuild / byPlatform are 7-day maps even though totalCrashes24h sits beside them.
Funnels & conditions
/api/v1/funnelsThe game's saved funnels — { funnels: [ { id, name, kind, steps } ] }/api/v1/funnelsBody { name, kind? (user | cross_actor), steps: 2–10 of eventName or eventName{key=value} } → 201 { id }/api/v1/funnels/{funnelId}Body any of { name?, kind?, steps? } → 200 { id }/api/v1/funnels/{funnelId}Removes the definition only → 200 { deleted, telemetryDeleted: false }/api/v1/funnels/{funnelId}/analyze?days=30Run it: entered, finished, per-step ladder, conversionPct + worstStep — both null when truncated (a short read inflates conversion); ?allowTruncated=true for marked figures/api/v1/audiencesBody { name, rules: [ { kind, … } ] } → 201 { id }; evaluate with read/audiences/members/api/v1/audiences/{audienceId}Removes the definition only → 200 { deleted, telemetryDeleted: false }Funnel steps accept attribute conditions in a canonical syntax shared by the funnel URL, saved funnels, and dashboard widgets: name alone, name{key=value} (equals), or name{key!=value} (not-equals). Multiple conditions are ;-separated and ANDed (max 4 per step); values are compared as strings; != also matches events that don't carry the key. The same key=value;key2!=value2 shape drives the page-wide ?attr= filter, and ?player= scopes the whole analytics page to one userId. In the UI a 3-dropdown picker (key / = ≠ / value) is fed by a persistent per-event attribute catalog, so attributes stay selectable even after their rows age out of the studio's retention window (30 days on the free tier, up to 90 configurable).
Symbols API
Upload debug symbols (PDB / dSYM / .sym) per build from CI so a symbol set is on file for every build, ready for native frames to resolve as that step ships — the tombstack-symbols CLI wraps these calls. Managed (C#) stack traces are already readable without any upload. Registration is idempotent on (game, build, debugId, module) and returns a presigned S3 POST for the file. POST needs the write scope: use a studio key (tmb_st_…) with ?gameId= — per-game tokens are ingest-only and 403 here.
/api/v1/symbols?gameId=…201 { upload: { url, fields } } — presigned S3 POST/api/v1/symbols?buildVersion=2.4.1200 { symbols: [ { buildVersion, moduleName, debugId, codeId, arch, os, appType, uploadedAt } ], symbolication: { serverSideFrameResolution, stage, note } }/api/v1/symbols/archive?gameId=…Body { fileName, buildVersion?, appType? } → 201 { uploadId, upload } — presigned S3 POST; the archive is extracted and indexed server-side/api/v1/symbols/archive/{uploadId}?gameId=…Poll the archive's processing → { status: { status, fileName, symbolCount, skippedCount, error } }/api/v1/symbols/minidumps/{crashId}?gameId=…{ minidump: { url, key, fileName, sizeBytes, storedAtIso, note } } — url is a 15-minute presigned download; 404 when none is stored (managed exceptions carry none)GET response carries a symbolication object — read it before you build on this endpoint. Uploaded symbols are indexed and stored per build, but Tombstack does not resolve native frames server-side yet, so native stacks still arrive as raw addresses however many modules are registered. serverSideFrameResolution is false and stage is "INDEXED" — never "READY". The note field also names the route that does work today: download the crash's minidump (GET /api/v1/symbols/minidumps/{crashId}) and stackwalk it locally against these symbols. A CI step that asserts "N modules registered" and reports success is measuring an upload, not a readable stack.MCP — hosted AI access
Tombstack hosts a remote MCP server — nothing to download, build, or run. Enable MCP access (€20/mo or $22/mo per studio) on your billing page, mint a key (tmb_mcp_…) scoped to all your games or a single game, and point Claude, Cursor, or any MCP client at the endpoint:
TrackMetric name, match list and match timeline, who was connected to a server, the log-pull queue, registered symbols, the minidump download that is the only route to frames for a native crash, and per-build release health carrying each build’s session denominator and this product’s own verdict — including the builds it refuses to grade. So an AI can both read and act. Funnels and audiences it creates are definitions, and show up on your Funnels dashboard; deleting one removes a saved question, never your telemetry. A key scoped to all games calls list_games first and passes gameId to each tool; a single-game key needs no gameId at all. Revoking a key or disabling the add-on cuts access instantly.SDKs & repositories
Everything you install ships from public repositories or straight from this site:
Unity SDK — AnkleBreaker-Studio/tombstack-unity (public). Install via UPM git URL https://github.com/AnkleBreaker-Studio/tombstack-unity.git#v0.19.6 — or, for the current release, grab the tarball from the download page.
Native C/C++ SDK — AnkleBreaker-Studio/tombstack-native (public, v0.9.1). C99 DLL ABI for any engine, with offline crash sidecars the CLI uploader drains; minidump capture is coming (Phase 2).
CLI — tombstack-doctor, tombstack-upload and tombstack-symbols, zero-dependency Node 18+, published to the public tombstack-cli mirror — run with npx --package github:AnkleBreaker-Studio/tombstack-cli … (no checkout, no install).