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.
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).
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.
{
"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.
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.
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