Draft, early spec

Skorble for game developers

This page describes phase 1 and may change before a stable release.

What Skorble is

Skorble is a shared identity, score, streak and leaderboard layer for daily puzzle and word games. It is not a game and does not host puzzles. Players sign in once with Qorble (qorble.com); your game either adds the Skorble script tag or calls the JSON API from its server. Skorble stores no emails or passwords.

Two integration paths

1. Embed (script tag) In progress: stub only

The planned drop-in for browser games. Today embed.js is a stub: it exposes window.Skorble and discovers the tags below, but does not sign in, submit or render widgets yet.

<script src="https://skorble.com/embed.js" data-site="SITE_ID"></script>
<div data-skorble="submit" data-score="4" data-puzzle-id="2026-10-16"></div>
<div data-skorble="leaderboard" data-scope="daily"></div>
<div data-skorble="streak"></div>

Planned flow: PKCE + popup (or redirect) to /embed/authorize, one-time code exchanged at POST /api/token for a site-scoped player token (skp_..., 30 days). Embed scores are client-reported (verified: false) unless your server signs them (see Score rules).

2. Server API (available now)

For games with their own accounts. Your server holds a site API key (sks_..., shown once on the owner page, rotatable). Keys are for servers only: a request carrying a key and a browser Origin header is refused.

  1. Start a link: POST /api/link {"external_player_id": "user-123"} returns a one-time link_code and link_path (valid 30 minutes).
  2. Send the player to https://skorble.com + link_path; they sign in with Qorble and confirm.
  3. Push scores: POST /api/scores with external_player_id. Server pushes are source: "server", verified: true.

Endpoint reference

All bodies are JSON. Auth is Authorization: Bearer sks_... (site key) or Bearer skp_... (player token). Errors are {"error": "snake_case_code"} with a fitting status; validation errors are 422.

MethodPathAuth
POST/api/scoressite key or player token
GET/api/leaderboardpublic (player token optional, adds me)
GET/api/meplayer token
GET/api/player/{id}/statspublic
POST/api/linksite key (start) or player token (complete)
POST / DELETE/api/tokennone (code exchange) / player token (sign out)

POST /api/scores

Body: puzzle_id (required, 1-64 chars A-Za-z0-9._:-, usually YYYY-MM-DD), score (required integer), data (optional object, max 16 KB), site (optional, must match), external_player_id (site key only, required there), signature (player token only). Returns 201 recorded, or 200 duplicate / updated.

curl -X POST https://skorble.com/api/scores \
  -H "Authorization: Bearer sks_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"puzzle_id":"2026-10-16","score":4,"external_player_id":"user-123","data":{"grid":"..."}}'

HTTP/1.1 201 Created
{
  "status": "recorded",
  "score": {"site": "clorble", "puzzle_id": "2026-10-16", "played_on": "2026-10-16", "score": 4,
            "source": "server", "verified": true, "submitted_at": "2026-10-16T12:00:00-04:00"},
  "player": {"id": "01JABCDEF0123456789XYZABCD", "name": "Ada"},
  "rank": 1,
  "streak": {"current": 1, "longest": 1, "played_today": true, "last_played_on": "2026-10-16"}
}

Errors include player_not_linked (404), external_player_id_required, site_mismatch, data_too_large, puzzle_date_in_future (422), invalid_signature (422), signature_required, embed_submissions_disabled, site_disabled (403).

GET /api/leaderboard

Daily board: ?site=clorble[&puzzle_id=2026-10-16][&limit=1..100] (default puzzle is the site-local date, default limit 25). Cross-site: ?scope=global[&date=YYYY-MM-DD], ranking players by distinct games played that day. Unknown site: 404 unknown_site.

curl "https://skorble.com/api/leaderboard?site=clorble&puzzle_id=2026-10-16"

{
  "site": {"id": "clorble", "name": "Clorble", "url": "https://clorble.com", "score_direction": "lower", "score_label": "guesses"},
  "scope": "daily", "puzzle_id": "2026-10-16", "total": 1,
  "entries": [{"rank": 1, "player": {"id": "01JABCDEF0123456789XYZABCD", "name": "Ada"}, "score": 4,
               "verified": true, "source": "server", "submitted_at": "2026-10-16T12:00:00-04:00"}]
}
// with a player token: "me": {"rank": 1, "score": 4, "verified": true}  (or null)
// scope=global: {"scope": "global", "date": "...", "entries": [{"rank": 1, "player": {...}, "games": 3}]}

GET /api/me

Player token. Optional ?puzzle_id= (defaults to the site-local date).

{
  "player": {"id": "01JABCDEF0123456789XYZABCD", "name": "Ada"},
  "streak": {"current": 5, "longest": 9, "played_today": true, "last_played_on": "2026-10-16"},
  "site": {
    "id": "clorble",
    "streak": {"current": 3, "longest": 7, "played_today": true, "last_played_on": "2026-10-16"},
    "puzzle_id": "2026-10-16",
    "score": {"site": "clorble", "puzzle_id": "2026-10-16", "played_on": "2026-10-16", "score": 4, "source": "embed", "verified": false, "submitted_at": "..."},
    "linked": false
  }
}

GET /api/player/{id}/stats

Public profile by public id (26-character ULID). Unknown: 404 unknown_player.

{
  "player": {"id": "01JABCDEF0123456789XYZABCD", "name": "Ada"},
  "totals": {"games": 1, "plays": 12},
  "streak": {"current": 5, "longest": 9, "played_today": true, "last_played_on": "2026-10-16"},
  "sites": [{"site": {"id": "clorble", "name": "Clorble", "url": "https://clorble.com", "score_direction": "lower", "score_label": "guesses"},
             "plays": 12, "best_score": 3, "current_streak": 3, "longest_streak": 7,
             "played_today": true, "last_played_on": "2026-10-16"}]
}

POST /api/link

// site key: {"external_player_id": "user-123"}
201 {"status": "pending", "link_code": "...", "link_path": "/link/...", "expires_at": "..."}
200 {"status": "linked", "external_player_id": "user-123", "player": {"id": "...", "name": "Ada"}}
// player token (same site): {"link_code": "..."}
200 {"status": "linked", "site": "clorble", "player": {...}}   404 invalid_link_code   409 on conflict

POST /api/token, DELETE /api/token

// {"site": "clorble", "code": "...", "code_verifier": "..."}
200 {"access_token": "skp_...", "token_type": "Bearer", "expires_at": "...", "player": {...}}
400 {"error": "invalid_grant"}
// DELETE with the player token
200 {"status": "revoked"}

Score rules

  • One score per player, site and puzzle. By default the first submission stands; later ones return duplicate with the stored score. A site set to best keeps the better score.
  • A server-verified score always supersedes a client-reported one; a client report never overwrites a verified score.
  • Each site sets whether higher or lower is better. Ranking is competition style (1, 1, 3); ties keep the earliest submission first.
  • Scores are integers; scale decimals yourself (for example milliseconds).
  • A date-shaped puzzle_id counts for that date (rejected if more than 1 day ahead of the site's today); any other id counts for the site-local day of submission.
  • Verified embed scores: your server signs v1|site|puzzle_id|score|player_public_id with HMAC-SHA256 (hex) using the site signing secret, and the embed passes it as signature. Sites can require signatures or disable embed submissions.

Streaks

Every streak object has exactly these fields: current (int), longest (int), played_today (bool), last_played_on (YYYY-MM-DD or null). Per-site streaks count consecutive play days in the site's time zone; the cross-site streak counts consecutive days with any Skorble play in the player's time zone (default America/New_York). A streak stays current through the next day and resets after a missed day. In /api/player/{id}/stats the per-site entries flatten these to current_streak, longest_streak, played_today, last_played_on.

CORS and origins

Each site registers an exact-match origin allowlist (for example https://clorble.com; plain http only for localhost). /api/* responses carry Access-Control-Allow-Origin only for registered origins, with methods GET, POST, DELETE, OPTIONS and headers Authorization, Content-Type, Accept. Credentials are never allowed: the API uses bearer tokens, not cookies. Player tokens are only accepted from their site's allowed origins.

Rate limits

  • POST /api/scores: 600/min per site key; 30/min per player token; plus 120/min per IP.
  • Reads (/api/leaderboard, /api/me, /api/player/{id}/stats): 120/min per IP.
  • /api/token: 20/min per IP. /api/link: 30/min per caller.

Over the limit you get HTTP 429.

Privacy

Skorble stores no emails or passwords. The API and pages expose only a player's public id (ULID) and display name; Qorble account ids stay internal. The optional score data blob is not returned by public endpoints. Sites see only the players who link with them or play on them.

Status and roadmap

  • Live: JSON API, Qorble sign-in, site registration (by invitation for now), account linking, server pushes.
  • In progress: the embed script (sign-in popup, auto-submit, leaderboard and streak widgets, skorble:ready / skorble:signedin / skorble:submitted events).
  • Later: anomaly flags and owner moderation, cached streaks, puzzle archives.

Clorble (clorble.com) is the first reference game.