Everything for building against GridHearth's public Sudoku puzzle pool.
The public API lives at https://sudoku-gen.gridhearth.com/api/v2/*.
It is entirely read-only — every endpoint is a GET over
the same puzzle pool GridHearth's own web UI reads, and nothing here
ever writes anything. The full machine-readable contract is at
/api/v2/openapi.json
(OpenAPI 3.0.3) — this page is the human-readable tour of it.
An API key is optional. Every endpoint works without one, at the anonymous rate-limit tier — there is no functionality gated behind a key, only a higher ceiling.
| Tier | Limit | How |
|---|---|---|
| Anonymous | 60 requests / minute (per IP) | default — no key needed |
| Keyed | 600 requests / minute (per key) | send X-API-Key |
| Pro | 6000 requests / minute | reserved for future use |
Send the key as an X-API-Key header (or ?api_key=
query param). A key that doesn't resolve to an issued one is rejected
with 401 rather than silently falling back to anonymous —
a typo in your key should never fail quietly. Keys are issued out of
band by the project operator (sudoku-engine apikey create --name ...
from the main repo's CLI) — there is no self-serve signup endpoint yet.
Every response carries X-RateLimit-Limit /
X-RateLimit-Remaining / X-RateLimit-Reset
headers. Exceeding the limit returns 429 with a
Retry-After header (seconds). Both client SDKs below parse
these into a rateLimit/rate_limit property
automatically.
| Method | Path | What it does |
|---|---|---|
| GET | /api/v2/health | Liveness check |
| GET | /api/v2/stats | Pool-wide puzzle counts by difficulty |
| GET | /api/v2/puzzles/random | A random puzzle, optionally ?difficulty=easy|medium|hard|expert |
| GET | /api/v2/puzzles/{id} | A specific puzzle by its canonical_id ("puzzle code") |
| GET | /api/v2/puzzles/{id}/solution | That puzzle's solution |
| GET | /api/v2/puzzles/{id}/image | A PNG of the grid — ?variant=solution for the solved grid |
| GET | /api/v2/pool-exports | NDJSON bulk-export snapshots available in R2 |
| GET | /api/v2/openapi.json | This API's OpenAPI document (no key needed, not rate-limited) |
v1 /api/v1/* is a permanent,
not-rate-limited alias of the same handlers, for anyone who wants the
versioned name without the rate limiting. unversioned
The original unversioned paths (/api/puzzles/..., no
version segment) still work too — that's what GridHearth's own web UI
calls — but new integrations should use /api/v2/*.
Handwritten, dependency-free clients for both endpoints above — see
sdk/python/ (stdlib
urllib only, Python 3.9+) and
sdk/typescript/
(platform fetch only, Node 18+/browsers) in the repo.
Neither is published to PyPI/npm yet — install from a checkout of the
repo per each package's own README.
Separate from everything above: the main sudoku-engine
Python package ships an MCP
server (sudoku-engine mcp, needs the mcp extra)
that exposes its local generate/solve/rate/explain/verify
engine as MCP tools for an AI agent (Claude Desktop, Claude Code, etc.)
running on your own machine. It talks to a local SQLite registry, not
this deployed API, and it generates new puzzles on demand rather than
reading from the existing pool. If you want an agent to read from
the live public pool instead, point it at the client SDKs above
(or the raw /api/v2/* endpoints); if you want it to
generate/solve/rate puzzles locally, that's what the MCP
server is for. See
the main README (section "4) ใช้เป็น
MCP server") for setup, including a Claude Desktop config snippet.
curl https://sudoku-gen.gridhearth.com/api/v2/puzzles/random?difficulty=hard
curl -H "X-API-Key: your-key-here" \
https://sudoku-gen.gridhearth.com/api/v2/stats
from gridhearth_sudoku_client import SudokuClient
client = SudokuClient() # or SudokuClient(api_key="...")
puzzle = client.random_puzzle(difficulty="hard")
solution = client.get_puzzle_solution(puzzle["canonical_id"])
print(client.rate_limit) # RateLimitInfo(limit=60, remaining=59, reset_seconds=41)
import { SudokuClient } from "gridhearth-sudoku-client";
const client = new SudokuClient(); // or { apiKey: "..." }
const puzzle = await client.randomPuzzle("hard");
const solution = await client.getPuzzleSolution(puzzle.canonical_id);
console.log(client.rateLimit); // { limit: 60, remaining: 59, resetSeconds: 41 }