Developers · Learndiag API v1

The Praxis 5001 question bank, as an API.

969 original practice questions across four subtests, bank statistics, and study-guide metadata — free, documented, and machine-readable. All read endpoints work anonymously; no account or key is needed to start.

Quickstart: anonymous GET in 60 seconds

One read endpoint with no credentials. Copy this into a terminal:

curl "https://learndiag.com/api/v1/questions/random?count=3&subtest=5003"

That returns three random original math questions as JSON. Health check: GET /api/v1/health. Full reference: /openapi.json (OpenAPI 3.1) or /api/openapi.yaml.

What the response looks like

Here is the shape of the two question endpoints. Field-by-field notes follow.

{
  "ok": true,
  "count": 3,
  "total_pool": 172,
  "items": [
    {
      "id": "5003-099",
      "subtest_code": "5003",
      "subtest": "Mathematics",
      "category": "Algebraic Thinking",
      "question": "What is the difference between an expression and an equation?",
      "options": [
        "An expression is a combination of terms; an equation states two expressions are equal",
        "Equations have variables; expressions don't",
        "They are the same",
        "Expressions have equals signs"
      ],
      "answer_index": 0,
      "answer_text": "An expression is a combination of terms; an equation states two expressions are equal",
      "explanation": "Expression: 3x + 2. Equation: 3x + 2 = 11. The equals sign distinguishes them."
    }
  ]
}

Field notes:

GET /api/v1/questions returns the same item shape inside a {"ok":true,"items":[...],"total":N} envelope for the full bank (paginated, see below).

Filtering and pagination

EndpointFiltersPagination
GET /api/v1/questionssubtest, category, searchlimit (1–100, default 20), offset
GET /api/v1/questions/randomcount (1–50, default 3), subtest, categorynone — draw is random
GET /api/v1/statsnonenone

Examples:

# first 20 science questions
curl "https://learndiag.com/api/v1/questions?subtest=5005"

# 10 reading questions from the "Reading" category
curl "https://learndiag.com/api/v1/questions?subtest=5002&category=Reading&limit=10"

# 5 random social-studies questions
curl "https://learndiag.com/api/v1/questions/random?count=5&subtest=5004"

search matches a plain text substring across question text; it does not do fuzzy or stemming matching.

Authentication and scoped API keys (optional)

Anonymous access is allowed on all read endpoints. There is one catch in the documentation: the OpenAPI spec lists ApiKeyAuth as the security for the read operations even though the implementation also accepts anonymous requests. Both work; the spec is stricter than real behavior. We are aligning the spec to express "anonymous or key" explicitly — until that ships, treat the spec's security as a description of what a key grants, not as a statement that anonymous is blocked.

If you want attribution and higher limits later, create a free scoped key — self-serve, no signup:

curl -X POST https://learndiag.com/api/v1/keys \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","scopes":["questions:read","stats:read"]}'

The key (tri_live_…) is returned once; store it. Send it as the X-API-Key header. Scope enforcement is real: a key without stats:read gets a structured forbidden_scope error from /api/v1/stats. Validate a key with GET /api/v1/keys/verify.

ScopeGrants access to
meta:readGET /api/v1/meta — subtest metadata and doc links
questions:readGET /api/v1/questions, GET /api/v1/questions/random
stats:readGET /api/v1/stats — per-category counts

Keys are not a frontend secret. If you call the API from a browser, the key is visible to anyone who opens dev tools. Use a key only from a server, or rely on anonymous access from the browser. CORS is fully open (Access-Control-Allow-Origin: *), so browser calls work anonymously today.

Rate limits

RowAnonymousWith API key
Key creation (POST /api/v1/keys)—10/hour per IP (enforced)
Read endpointsNo fixed public number today — documented here once a number existsHigher than anonymous; exact numbers follow the same policy
429 responseHTTP 429 with the standard error envelope; message says limits reset hourly
Retry-AfterNot currently set on 429 responses — a gap we are closing

This table is intentionally conservative: the only number we can point to in the code today is the 10/hour/IP key-creation limit. Read-side limits exist at the edge but are not exposed as a number in the implementation, so we are not inventing one for the docs.

Error format

Application-level API errors use one JSON envelope — never an HTML page:

{
  "error": {
    "code": "invalid_email",
    "message": "The email address is not valid.",
    "hint": "Provide a valid email address such as [email protected]."
  },
  "status": 400
}

Errors you are likely to hit: invalid_api_key (401), forbidden_scope (403, with details listing the required and present scopes), invalid_email (400), and too_many_requests (429). One caveat: this envelope covers application-layer errors. A CDN, proxy, or upstream failure can return a different body — treat "every error is this JSON" as true for the API code, not for the whole edge stack.

MCP server (Model Context Protocol)

Agents can call Learndiag over MCP Streamable HTTP at https://learndiag.com/mcp. The server negotiates protocol versions 2025-06-18 (latest) and 2025-03-26, and supports session IDs via the Mcp-Session-Id header.

Minimal client handshake (HTTP):

POST https://learndiag.com/mcp
Content-Type: application/json
Mcp-Protocol-Version: 2025-06-18

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo","version":"1.0"}}}

# then
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

Tools: list_subtests, get_questions, get_random_questions, get_bank_stats, list_study_guides — all read-only, anonymous. Client config examples: Claude Desktop uses {"mcpServers":{"learndiag":{"url":"https://learndiag.com/mcp"}}}; curl-based tools typically send Accept: application/json, text/event-stream and handle SSE if the server streams. Manifests for agent discovery: /.well-known/mcp/manifest.json and /.well-known/mcp-manifest.json.

Agent integration extras

Endpoints

Method & pathPurposeAuth
GET /api/v1/healthUptime probe + endpoint indexAnonymous
GET /api/v1/metaSubtest names, categories, counts, doc linksAnonymous or meta:read
GET /api/v1/statsQuestion counts per subtest/categoryAnonymous or stats:read
GET /api/v1/questionsPaginated bank — filters: subtest, category, search; limit ≤ 100, offsetAnonymous or questions:read
GET /api/v1/questions/randomRandom draw — count ≤ 50Anonymous or questions:read
POST /api/v1/keysCreate scoped API key (rate limit: 10/hour/IP)Anonymous
GET /api/v1/keys/verifyValidate a key, list its scopesKey

Account endpoints used by the web app (/api/register, /api/login, /api/verify, /api/state…) are documented in the same OpenAPI file and use JWT bearer tokens.

Documentation links

Version and change tracking are separate from the docs above: the API version is emitted by /api/v1/health, the OpenAPI file carries its own info.version, and substantive changes should be reflected in the changelog note on this page rather than a rolling date.

Licensing and attribution

All questions and explanations are original to Learndiag (not recycled official ETS items) and are provided through this API for programmatic use. What that covers, concretely:

"Free to surface" is not the whole license. The bullet list above is the readable version of the contractual language in the terms of service; if the two ever seem to conflict, the Terms govern.

Status & support

Uptime probe: /api/v1/health. Questions, bug reports, or rate-limit increases: email [email protected] with subject "API" — see contact page. For integration questions that are not API-specific, the contact page also routes to the right channel.