prep50 Explanations API

Textbook-referenced explanations for past questions, for prep50's own products. Send question ids; get back the explanation, the books and printed pages it cites, and the model's verdict on the recorded answer.

Base URL: https://engine.prep50.ng. This page: /. Interactive reference with every field: /docs.

Authentication

Every request carries your key in a header:

X-API-Key: p50_xxxxxxxx_…

Keys are issued per product by the prep50 engineering team and shown once. Keep the key on your server, never in a mobile app or browser page: anyone who extracts it can spend your budget. If your app needs explanations, your backend calls this API and passes the result on.

A key can be rotated without downtime: ask for a new one and the old one keeps working for 24 hours. If a key may have leaked, ask for it to be revoked at once.

Responses to auth problems: 401 missing, malformed, revoked or expired key; 429 more than your rate limit (default 60 requests/minute); 402 your daily model budget is used up (default $5/day; cached results stay free).

Asking for explanations

POST /v1/explanations with up to 500 question ids:

curl -s -X POST https://engine.prep50.ng/v1/explanations \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"question_ids": [11048, 86552, 86406], "callback_url": "https://yourapp.example/hook"}'

Answer (202): a job. Questions explained before, by anyone, are in results already with cached: true at no cost; the rest are queued. A new question takes about 6 seconds.

{"job_id": "7d7fbfb9-…", "status": "queued", "total": 3, "finished": 2, "results": [ … ]}

Then either poll GET /v1/jobs/{job_id} until status is done or failed, or give a callback_url: when the job ends we POST {"job_id", "status", "total", "finished", "error", "results_url"} to it (three attempts).

One question that already has an explanation: GET /v1/explanations/{question_id} (404 if none yet).

Your usage: GET /v1/usage — requests, questions, tokens and spend per UTC day, and today's spend against your budget.

Service health: GET /v1/health (no key) — 200 when the database and the worker are fine, 503 otherwise.

Reading a result

{
  "question_id": 86552,
  "status": "ready",
  "hold_reason": null,
  "review_required": false,
  "explanation": "The question asks for the best method to purify copper. … So the correct answer is **C) electrolyzing a copper (II) salt solution…**\n\n**Textbook reference:** *Understanding Chemistry*, pp. 201, 346",
  "references": [{"book": "Understanding Chemistry", "pages": "201, 346"}],
  "key_verdict": "AGREE",
  "cited_chunk_ids": [41822, 42107],
  "cached": true,
  "cost_usd": 0.0,
  "generated_at": "2026-10-28T19:40:12+00:00"
}
status Meaning What to do
ready Grounded in a textbook passage, consistent with the recorded answer Show it
held A draft exists but something is doubtful; see hold_reason Do not show it to students. Have a person read it.
skipped Not attempted: no answer key, or an explanation exists elsewhere Nothing
failed Model call failed after retries Retry later

Hold reasons you will meet: key_disagree: the model worked the question and reached a different answer from the recorded one; often the recorded key is wrong, so worth a look. has_diagram / stem_refers_to_diagram: the question needs a picture the model cannot see. no_supporting_passage: the books don't cover it. mentions_source: the wording talks about "the passage"; the content is usually fine, the wording is not student-facing.

explanation is Markdown with $…$ maths, written for a Nigerian secondary-school student, with the references line already appended. references is the same information as data. Citations are never invented: the model can only cite passages it was given, and page numbers come from the database at generation time.

key_verdict is the model's independent view of the recorded answer; it is AGREE on every ready result.

Limits and billing

Per key: a request rate (429 beyond it) and a daily model budget (402). Model cost is about $0.002 per new question; cached questions are free. Spend is drawn from prep50's central AWS credit and metered per key, visible at /v1/usage.

For prep50 engineering

Runs on the prep50-api EC2 instance (us-east-1, fixed IP 32.200.190.181) behind Cloudflare → Caddy → uvicorn. Two services, prep50-api (HTTP) and prep50-api-worker (jobs), under systemd; install and update with api/deploy/install.sh; Bedrock access through the instance role prep50-api-bedrock, no keys on the box.

Keys, on the instance:

python3 api/keys.py create "Prep50 Mobile" --rate 60 --budget 5
python3 api/keys.py list
python3 api/keys.py rotate "Prep50 Mobile"       # old key lives 24 h more
python3 api/keys.py revoke "Prep50 Mobile"
python3 api/keys.py budget "Prep50 Mobile" 20

Results are stored in rag_question_explanations with test_set = 'api:<client>', next to the studio's own runs, so the review pages see API output, and explain_questions.py apply can publish ready ones into prep50's question bank. The API itself never writes to the question bank.

Interactive reference with every field: /docs · OpenAPI: /openapi.json