Kerata API
Check code for AI from scripts, CI or your learning platform. A small REST API over HTTPS that returns JSON.
Quick start
Create a key under Account → API keys, keep it in an environment variable, and call the API with it:
export KERATA_API_KEY="paste your key here"
curl -s https://kerata.org/api/v1/me -H "Authorization: Bearer $KERATA_API_KEY"Base URL: https://kerata.org/api/v1. Requests and responses are JSON unless an endpoint takes a file upload.
Authentication
Send your key in the Authorization header as a bearer token (X-API-Key: <key> works too). A key acts as you, inside the workspace that was active when you created it. Keys are shown once, when you create them, and Kerata stores only a hash; if you lose one, revoke it and create another. Keep keys out of source control and client-side code.
Authorization: Bearer <your API key>Limits
Kerata is free during the public beta. Every account gets the same allowance:
| Requests a minute, per key | API calls a month | Checks a month | Class checks a month | Students per class check |
|---|---|---|---|---|
| 10 | 2,000 | 2,000 | 20 | 400 |
Over the per-minute limit you get 429 with a Retry-After header; over a monthly allowance, 402. Your current usage is on Usage & plan and in GET /api/v1/me.
Errors
Errors return a JSON body {"detail": "a readable message"} and a meaningful status code:
400 | The request is malformed (for example an unsupported URL). |
401 | The key is missing, revoked or mistyped. |
402 | A monthly allowance is used up. |
403 | The key's account isn't allowed to do this. |
404 | Not found, or a result your key can't see. |
409 | The result isn't ready yet; keep polling. |
413 | The upload is too large. |
422 | The input isn't valid, or the text isn't source code. |
429 | Too many requests, or the service is busy; retry after the delay given. |
503 | Checking is temporarily unavailable; try again shortly. |
Scans
GET/api/v1/me
Who the key belongs to
The key's owner, workspace, plan, and this month's usage and limits.
POST/api/v1/scan
Check pasted code
Body: code (the source, at least 5 lines) and an optional filename, which helps detect the language. The result is saved to your history.
curl -s https://kerata.org/api/v1/scan -H "Authorization: Bearer $KERATA_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile code solution.py '{code: $code, filename: "solution.py"}')"POST/api/v1/scan/upload
Check a source file or a .zip of a project
Multipart form with a file field. A single file can be up to 1 MB (its first 200 KB is checked); a zip can hold up to 80 source files.
curl -s https://kerata.org/api/v1/scan/upload -H "Authorization: Bearer $KERATA_API_KEY" -F file=@project.zipPOST/api/v1/scan/github
Check a public GitHub repository, folder, pull request or commit
Body: url. For a pull request or commit, only the lines it adds are scored. Add "history": true to also look at the repository's commit history; the response then includes history_job, which you poll at /api/v1/jobs/{id}.
curl -s https://kerata.org/api/v1/scan/github -H "Authorization: Bearer $KERATA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://github.com/owner/repo/pull/123"}'Class checks
A class check compares a whole assignment: AI likelihood for every submission, similarity between students, matches against your past terms and, when the export has several attempts, edit history. It runs in the background: start it, then poll until it's done.
POST/api/v1/class-checks
Start a class check
Multipart form. file is the submissions export: a zip with a folder or file per student, a Canvas “Download Submissions” zip, a Gradescope export, or PrairieLearn downloads.
| Field | Default | What it does |
|---|---|---|
file | required | The submissions export (.zip, or a PrairieLearn .csv) |
starter | none | Starter code to leave out of the similarity comparison |
extra | none | PrairieLearn submissions CSV, which adds attempt times |
assignment | file name | A name for the check |
peer, ai, history, corpus_past | true | Which checks to run |
lang | auto | Force a language |
course_id, assignment_id | none | File the check under one of your courses |
curl -s https://kerata.org/api/v1/class-checks -H "Authorization: Bearer $KERATA_API_KEY" \
-F file=@hw3_submissions.zip -F starter=@starter.zip -F assignment="HW 3: linked lists"
# {"id": "u3x...", "status": "queued", "url": "/api/v1/jobs/u3x..."}GET/api/v1/class-checks/{id}
Status, then the ranked student list
status goes from queued to running (with progress from 0 to 1) to done or error. When it's done, result.table lists the students in review order, each with the reasons it was ranked there.
ID=u3x...
until [ "$(curl -s https://kerata.org/api/v1/class-checks/$ID -H "Authorization: Bearer $KERATA_API_KEY" | jq -r .status)" = done ]; do sleep 3; done
curl -s https://kerata.org/api/v1/class-checks/$ID -H "Authorization: Bearer $KERATA_API_KEY" \
| jq '.result.table[] | {student, tier: .priority.tier, reasons: [.priority.signals[].text]}'GET/api/v1/class-checks/{id}/students/{student}
Evidence for one student
The student's files with their highlighted lines, the closest matches with other students, and edit history when there is one. GET /api/v1/class-checks/{id}/pair?a=…&b=… returns two students' matched passages side by side.
GET/api/v1/class-checks/{id}/export.csv
Export the student list
Also available as export.json with the full results. Exports are recorded in your workspace's audit log.
curl -s https://kerata.org/api/v1/class-checks/$ID/export.csv -H "Authorization: Bearer $KERATA_API_KEY" -o hw3.csvSaved results
GET/api/v1/results
Your saved scans and class checks, newest first
Filter with kind (scan or class) and course_id; page with limit (up to 200) and before set to the previous response's next. GET /api/v1/results/{id} returns one result.
curl -s "https://kerata.org/api/v1/results?kind=class&limit=20" -H "Authorization: Bearer $KERATA_API_KEY"A result is visible to the person who ran it, the workspace's owners and admins, and the instructors and TAs of the course it's filed under. Anything else returns 404.
Reading a result
A scan response (abridged):
{"overall": 0.97, "verdict": "likely AI", "confidence": "high", "reliable": true,
"thresholds": {"ai": 0.96, "mixed": 0.44},
"files": [{"filename": "solution.py", "overall": 0.97, "highlight": true, "line_probs": [0.12, 0.91, "…"]}],
"scan_id": "Qm9…"}overallis the AI likelihood from 0 to 1.verdictis one oflikely AI,contains AI-like section,mixed / uncertain,likely humanortoo short to judge reliably.- Read the cutoffs from
thresholdsin each response instead of hard-coding them: they change when the detector is updated. - When
reliableisfalse(very short code), show the verdict, not a score. line_probsrank lines for highlighting within a file; they aren't calibrated probabilities. Only shade lines whenhighlightis true.attribution, the AI tools the style is most similar to, is present only when the verdict islikely AI. Treat it as a lead.- Text that isn't source code is refused with
422.
Scores are evidence for a person to review. Please show them that way in your own tools: see How it works.
OpenAPI schema
For code generators and API clients: the machine-readable schema is at /openapi.json, with an interactive explorer at /docs.