Authentication
How to authenticate requests to the Tabletop League API.
Every authenticated endpoint accepts one of two credentials:
- A personal API key —
Authorization: Bearer ttl_user_…— for scripts and integrations. - A logged-in browser session (Supabase JWT cookie) — handy for one-off
curlcalls, and required for key management.
The public competitions endpoints are the exception: they need no credential at all. Don't send a key to them; a key baked into a shared mod or script is readable by everyone who has it.
Keys carry no scopes. A key acts as you, and only ever returns your own data or public aggregate data. There is no read/write/admin split.
Early access — key minting is gated
A working personal API key needs two entitlements on your account:
api.personal.read, which lets you mint a key, and
api.personal.rate_limit above 0, which is your hourly request
allowance. With api.personal.read alone you can mint a key, but
every call with it returns 403 "No rate-limit allowance". Both are off by
default while the Personal API is in early access. Contact us to enable them
on your account. You don't need a key to download the aggregate statistics
exports — a signed-in session works. See
Public statistics exports.
Minting a key
There is no settings screen for keys — mint and manage them through the key-management API, authenticated with your browser session cookie. (Bearer keys can't manage keys, so a leaked key can't mint more.)
curl -X POST https://tabletopleague.com/api/v1/me/keys \
--cookie cookies.txt \
-H "Content-Type: application/json" \
-d '{"name":"my-integration","terms_version":"2026-01-01","terms_accepted":true}'
The plaintext key is returned once — store it immediately, it can't be retrieved again:
{
"success": true,
"data": {
"id": "…",
"plaintext": "ttl_user_…",
"key_prefix": "ttl_user_…",
"rate_limit_rph": 60
},
"_meta": { "request_id": "…", "timestamp": "…", "terms_url": "/api-terms" }
}
Request body:
| Field | Required | Notes |
|---|---|---|
name | yes | A label so you can recognize the key later. |
terms_version | yes | The API terms version you're accepting — see /api-terms. |
terms_accepted | yes | Must be true; records your acceptance of the terms. |
rate_limit_rph | no | Per-hour request cap for this key (default 60). |
Every key starts with the ttl_user_ prefix.
Using a key
Send it as a Bearer token. This is the only supported way to pass a key — never put it in a query string:
curl -H "Authorization: Bearer ttl_user_xxxxx" \
https://tabletopleague.com/api/v1/me/stats
Listing and revoking keys
Both require your session cookie (not a Bearer key):
# List active keys — prefixes and metadata only, never the secret
curl --cookie cookies.txt https://tabletopleague.com/api/v1/me/keys
# Revoke a key by its id (from the list above)
curl -X DELETE --cookie cookies.txt \
https://tabletopleague.com/api/v1/me/keys/<key-id>
Rate limits
The Personal API applies a per-hour cap (default 60 requests/hour) and
returns X-RateLimit-* headers. The public statistics exports add a separate
30 requests/minute per IP limit, and the public competitions endpoints allow
60 requests/minute per IP. Exceeding either returns 429 with an
X-RateLimit-Reset epoch second — honor it.
Where each credential works
| Surface | Accepts |
|---|---|
Personal API (/api/v1/me/*) | Bearer key or session cookie |
Public statistics exports (/api/v1/public/hero-stats*) | Bearer key or session cookie |
Key management (/api/v1/me/keys) | Session cookie only |
Public competitions (/api/v1/public/competitions*) | Nothing — no credential needed |
Read the acceptable use terms before you build — publicly posting the aggregate exports requires attribution.