GridHearth Sudoku — Developer docs

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.

Authentication & rate limits

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.

TierLimitHow
Anonymous60 requests / minute (per IP)default — no key needed
Keyed600 requests / minute (per key)send X-API-Key
Pro6000 requests / minutereserved 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.

Endpoints

MethodPathWhat it does
GET/api/v2/healthLiveness check
GET/api/v2/statsPool-wide puzzle counts by difficulty
GET/api/v2/puzzles/randomA 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}/solutionThat puzzle's solution
GET/api/v2/puzzles/{id}/imageA PNG of the grid — ?variant=solution for the solved grid
GET/api/v2/pool-exportsNDJSON bulk-export snapshots available in R2
GET/api/v2/openapi.jsonThis 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/*.

Client SDKs

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.

MCP server

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.

Examples

curl

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

Python

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)

TypeScript / JavaScript

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 }