Skip to main content
Documentation

Authentication

How to authenticate requests to the Tabletop League API.

Every authenticated endpoint accepts one of two credentials:

  • A personal API keyAuthorization: Bearer ttl_user_… — for scripts and integrations.
  • A logged-in browser session (Supabase JWT cookie) — handy for one-off curl calls, and required for key management.

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

Minting a personal API key requires the api.personal.read entitlement, which is off by default while the Personal API is in early access. Contact us to enable it 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. 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 |

Read the acceptable use terms before you build — publicly posting the aggregate exports requires attribution.