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.
- Start a link:
POST /api/link {"external_player_id": "user-123"}returns a one-timelink_codeandlink_path(valid 30 minutes). - Send the player to
https://skorble.com+link_path; they sign in with Qorble and confirm. - Push scores:
POST /api/scoreswithexternal_player_id. Server pushes aresource: "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.
| Method | Path | Auth |
|---|---|---|
| POST | /api/scores | site key or player token |
| GET | /api/leaderboard | public (player token optional, adds me) |
| GET | /api/me | player token |
| GET | /api/player/{id}/stats | public |
| POST | /api/link | site key (start) or player token (complete) |
| POST / DELETE | /api/token | none (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
duplicatewith the stored score. A site set tobestkeeps 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
higherorloweris 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_idcounts 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_idwith HMAC-SHA256 (hex) using the site signing secret, and the embed passes it assignature. 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:submittedevents). - Later: anomaly flags and owner moderation, cached streaks, puzzle archives.
Clorble (clorble.com) is the first reference game.