API reference
Everything your product does with MacroEval goes through https://macroeval.com/api/v1: fetch the rubric to judge with, log each case and its verdict, record people's labels, and run agreement reports. Each key reaches exactly one project. New fields and endpoints may be added; existing ones keep their meaning.
Authentication
Every request carries a project key as a bearer token. Sign in, pick the project in the top bar, and open Settings → Developers to create one. It is shown once: MacroEval keeps only a hash of it, so a lost key can't be recovered. Revoke it there and create another. Revoking takes effect on the next request.
curl https://macroeval.com/api/v1/rubrics/support-reply \
-H "Authorization: Bearer me_live_…"- A key is
me_live_followed by 32 letters and digits. Anything else is refused as malformed. - A key reaches one project's rubrics, cases and reports, and nothing else. No parameter picks another project.
- Keep keys on your servers. A key in browser or mobile code is public. Use one key per system, so revoking one never breaks the others.
Your first request
Fetch a rubric's current version. It's the call your product makes before it judges anything, and it tells you at once whether the key and the slug are right.
curl https://macroeval.com/api/v1/rubrics/support-reply -H "Authorization: Bearer $MACROEVAL_API_KEY"
{
"slug": "support-reply",
"version": 3,
"instructions": "Judge the reply against the order on file.",
"rules": ["Never promise a refund the order doesn't qualify for", "Answer the question that was asked"],
"model": null
}A 404 means the project has no rubric with that slug yet. Create its first version with POST /rubrics/{slug}/versions, or under Rubrics in the app.
Conventions
Requests and responses are JSON. Send Content-Type: application/json. Every error has the same shape, with the HTTP status telling you what kind it is.
{ "error": "rules must be an array of strings" }| 400 | The body or a path value failed validation. The message names the field. Unknown fields are refused too, so a typo fails loudly instead of being ignored. |
| 401 | The key is missing, malformed or revoked. |
| 404 | Nothing with that slug, version, externalId or id exists in the key's project. Other projects' records are never visible. |
| 413 | The body is over 192 KB. |
| 500 | Something went wrong on our side. Retrying a case is safe, since cases are keyed by externalId. A retried judgment or label is stored again, and reports use the latest one per case. |
Identifiers
| slug | A rubric's name: kebab-case (a-z, 0-9, -), up to 64 characters, such as support-reply. |
| externalId | Your own id for a case, up to 200 characters of A-Z a-z 0-9 . _ : ~ @ + = , -. No slashes, percent signs or spaces, so it is always one path segment. |
| version | A positive integer. Versions are numbered in order, never reused and never rewritten. |
Limits
| instructions | Up to 20,000 characters. |
| rules | Up to 60 rules, each up to 300 characters. |
| model | A model id, up to 100 characters. |
| notes | Up to 2,000 characters. |
| reasons | Up to 20 per judgment, each up to 500 characters. |
| label reason | Up to 500 characters; labeler up to 200. |
| context | Any JSON object up to 8 KB. |
| body | Any request up to 192 KB. |
Case parts
A case is a list of parts, read in order. Each has a type and a role, and the role is your name for what the part is: question, reply, reference, output. Roles are lowercase letters, digits, _ and -, up to 32 characters, and appear in the judge's prompt and on the cases page.
| text | { text }, up to 32,000 characters. A question, a reply, a transcript, a plan. The judge reads it. |
| json | { data }, any JSON value up to 32 KB. An order, tool calls, scores, a ranked list. The judge reads it, pretty-printed. |
| image | { url }. A render, a generated photo, a screenshot. The judge sees it. |
| video | { url, frames }, with frames a list of image URLs you sampled from it, in order. Reviewers can play the video; the judge sees the frames. |
| file | { url, mediaType, name? }. A 3D model, a structure, a document. Reviewers can open it, and glTF models and PDB, mmCIF and SDF structures open in a built-in viewer. The judge is only told the file exists. |
A case may have up to 12 parts and 6 images in all, counting video frames, since each one is something the judge looks at. Its text and JSON together may total 96 KB. Every URL must be public https, up to 2,048 characters. MacroEval fetches images and frames when it regrades, and files only when a reviewer opens them, up to 25 MB.
The judge reads text, JSON, images and frames, and can't open files. So send it what it should judge as parts of their own: renders of a 3D model, a structure's scores as JSON, frames of a video. A case with nothing the judge can read is reported as an error rather than given a verdict.
Cases logged with the older images: [{ role, url }] field keep working. Those images become image parts, ahead of any parts sent alongside.
Endpoints
10 endpoints in three groups. Every path is relative to https://macroeval.com.
Rubrics
GET/api/v1/rubrics/{slug}
The rubric's current version: what your product should judge with right now.
Returns
200 { slug, version, instructions, rules, model }- model is null unless the version names one.
- 404 when the project has no rubric with that slug.
GET/api/v1/rubrics/{slug}/versions/{v}
One version by number, for a product pinned to it rather than following the current one.
Returns
200 { slug, version, instructions, rules, model }POST/api/v1/rubrics/{slug}/versions
Saves a new version, and creates the rubric with its first one. Versions are numbered for you and never rewritten.
{ "instructions": "Judge the reply against the order on file.", "rules": ["Never promise a refund the order doesn't qualify for"], "model": "Qwen/Qwen3-VL-235B-A22B-Instruct", "notes": "Tighter refund rule after ticket 812", "current": false }Returns
201 { version }- rules is required; blank entries are dropped.
- current defaults to true. Send false to try a version before your product uses it.
- model is optional. MacroEval's regrade uses it when it is a DeepInfra model id (owner/name) and its own default otherwise.
Cases
POST/api/v1/cases
Logs one output your product judged. Keyed by your own externalId, so sending it again updates the case in place.
{ "externalId": "ticket-812", "rubric": "support-reply", "parts": [ { "type": "text", "role": "question", "text": "My blender arrived broken. Refund?" }, { "type": "text", "role": "reply", "text": "I've refunded your card in full." }, { "type": "json", "role": "order", "data": { "delivered_days_ago": 41, "refund_window_days": 30 } } ], "context": { "channel": "email" } }Returns
201 { id } for a new case, 200 { id } for an update- See Case parts for every part type and its limits.
- The older images: [{ role, url }] field still works; its images come first, as image parts.
- context is any JSON object, kept for you and never shown to the judge.
POST/api/v1/cases/{externalId}/judgments
Records your product's own verdict on a case, for the rubric version it judged with.
{ "rubricVersion": 3, "verdict": "fail", "reasons": ["promises a refund outside the 30-day window"], "broken": ["Never promise a refund the order doesn't qualify for"] }Returns
201 { id }- 404 when the case, or that version of its rubric, doesn't exist.
- reasons and broken are optional lists.
POST/api/v1/cases/{externalId}/labels
Records a person's verdict. With addRule on a fail, the reason also becomes a rule in a new current version.
{ "verdict": "fail", "reason": "Offers a refund the policy doesn't allow", "labeler": "reviewer@example.com", "addRule": true }Returns
201 { id, ruleVersion?, ruleError? }- A rule equal to an existing one (ignoring case and spacing) isn't added twice.
- The label is always kept. If its reason can't become a rule, ruleError says why.
Reports
POST/api/v1/rubrics/{slug}/reports
Starts an agreement report for a version (the current one when you send no body) and runs its first slice.
{ "version": 4, "model": "Qwen/Qwen3-VL-30B-A3B-Instruct" }Returns
202 { id, status: "running" }, or 201 when it finished in the first slice- model is optional: one of the models on the compare page's price list. The report then regrades on that model only (no fallback) and counts what it cost.
GET/api/v1/rubrics/{slug}/reports/{id}
A report. Reading a running report also runs its next slice, so polling is what moves it forward.
Returns
200 { id, rubric, rubricVersion, createdAt, status, n, product, regrade, perCase, progress, costUsd, model?, tokens?, error? }GET/api/v1/rubrics/{slug}/reports/latest
The newest report that finished, in the same shape.
Returns
200 { … } as above- 404 until a report of that rubric is ready.
GET/api/v1/rubrics/{slug}/compare
Models side by side: the newest ready report of each model the rubric was replayed on, next to how your product's own grader did on the same labelled cases.
Returns
200 { rubric, product, productReportId, rows: [{ model, label, reportId, createdAt, n, regrade, costUsd, costPerCaseUsd }] }- Rows are sorted by fail recall, then cost. Only reports that named a model appear as rows.
Reports and polling
A report freezes the latest label of up to 200 labelled cases of a rubric when it starts, so labels added later don't shift it. It compares your product's verdicts for that version with the labels, and regrades every case with the version so you can see how it would do.
Regrading takes time, so a report advances in slices of about 25 seconds while you poll. The POST runs the first slice, and every GET of a running report runs the next one. Poll until status is no longer running. A report nobody polls waits where it stopped. The app polls for you while a report's page is open.
| status | running, then ready, or error when the report itself couldn't run (its error says why). |
| n | How many labelled cases the report froze when it started. |
| progress | { done, total }: cases regraded or failed so far. |
| product | How your product's own verdicts agree with the labels, or null when it logged none for this version. |
| regrade | How MacroEval's regrade with this version agrees with the labels. |
| perCase | One row per case: externalId, label, product, regrade, the judge's reasons, and error when that case couldn't be regraded. |
The agreement numbers
Both product and regrade carry the same numbers. A fail is the positive class, because a missed reject is the costly mistake. A ratio with nothing to count is null, never 0.
| failRecall | Of the cases people failed, the share the grader also failed. Low means rejects slip through. |
| failPrecision | Of the cases the grader failed, the share people failed too. Low means false alarms. |
| agreement | The share of cases where the grader and the label match. |
| confusion | { tp, fp, fn, tn }, with a fail as the positive class: tp both failed, fp only the grader failed, fn only people failed, tn both passed. |
{
"id": "6630f1c2a9d04b5e8e1f2a77",
"rubric": "support-reply",
"rubricVersion": 4,
"status": "ready",
"n": 120,
"product": { "n": 118, "agreement": 0.78, "failRecall": 0.64, "failPrecision": 0.82, "confusion": { "tp": 23, "fp": 5, "fn": 13, "tn": 77 } },
"regrade": { "n": 120, "agreement": 0.9, "failRecall": 0.91, "failPrecision": 0.87, "confusion": { "tp": 33, "fp": 5, "fn": 3, "tn": 79 } },
"perCase": [
{ "externalId": "ticket-812", "label": "fail", "product": "pass", "regrade": "fail", "reasons": ["refund outside the 30-day window"] }
],
"progress": { "done": 120, "total": 120 }
}Example values.
Recipes
Log a case and your product's verdict
Log the case first, then the verdict, naming the version your product judged with.
curl -X POST https://macroeval.com/api/v1/cases \
-H "Authorization: Bearer $MACROEVAL_API_KEY" -H "Content-Type: application/json" \
-d '{"externalId":"ticket-812","rubric":"support-reply","parts":[
{"type":"text","role":"question","text":"My blender arrived broken. Refund?"},
{"type":"text","role":"reply","text":"I have refunded your card in full."},
{"type":"json","role":"order","data":{"delivered_days_ago":41,"refund_window_days":30}}]}'
curl -X POST https://macroeval.com/api/v1/cases/ticket-812/judgments \
-H "Authorization: Bearer $MACROEVAL_API_KEY" -H "Content-Type: application/json" \
-d '{"rubricVersion":3,"verdict":"pass"}'Video, 3D models and structures
Send what the judge should look at as images or frames, and the original as a file for reviewers.
"parts": [
{ "type": "video", "role": "clip", "url": "https://cdn.example/shot-4.mp4",
"frames": ["https://cdn.example/shot-4-0s.jpg", "https://cdn.example/shot-4-2s.jpg", "https://cdn.example/shot-4-4s.jpg"] }
]
"parts": [
{ "type": "image", "role": "render", "url": "https://cdn.example/kitchen-front.png" },
{ "type": "file", "role": "model", "url": "https://cdn.example/kitchen.glb", "mediaType": "model/gltf-binary", "name": "kitchen.glb" }
]
"parts": [
{ "type": "json", "role": "scores", "data": { "mean_plddt": 84.2, "interface_contacts": 31 } },
{ "type": "file", "role": "structure", "url": "https://cdn.example/design-7.cif", "mediaType": "chemical/x-mmcif" }
]Try a version before it's live
Save the candidate without making it current, report on it, and compare its regrade with the current version's. Your product keeps judging with the current version throughout. If the candidate does better, make it current under Rubrics in the app, or save it again with current: true.
V=$(curl -s -X POST https://macroeval.com/api/v1/rubrics/support-reply/versions \
-H "Authorization: Bearer $MACROEVAL_API_KEY" -H "Content-Type: application/json" \
-d '{"instructions":"…","rules":["…"],"current":false}' | jq .version)
ID=$(curl -s -X POST https://macroeval.com/api/v1/rubrics/support-reply/reports \
-H "Authorization: Bearer $MACROEVAL_API_KEY" -H "Content-Type: application/json" \
-d "{\"version\":$V}" | jq -r .id)
until [ "$(curl -s https://macroeval.com/api/v1/rubrics/support-reply/reports/$ID \
-H "Authorization: Bearer $MACROEVAL_API_KEY" | tee report.json | jq -r .status)" != running ]; do sleep 2; done
jq '.regrade | {failRecall, failPrecision, agreement}' report.jsonFetch the rubric without depending on MacroEval
Cache the rubric for a few minutes and keep a copy bundled with your product. If MacroEval is unreachable, judge with the copy and log the case later. MacroEval should never sit in the path your users wait on.