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" }
400The 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.
401The key is missing, malformed or revoked.
404Nothing with that slug, version, externalId or id exists in the key's project. Other projects' records are never visible.
413The body is over 192 KB.
500Something 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

slugA rubric's name: kebab-case (a-z, 0-9, -), up to 64 characters, such as support-reply.
externalIdYour 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.
versionA positive integer. Versions are numbered in order, never reused and never rewritten.

Limits

instructionsUp to 20,000 characters.
rulesUp to 60 rules, each up to 300 characters.
modelA model id, up to 100 characters.
notesUp to 2,000 characters.
reasonsUp to 20 per judgment, each up to 500 characters.
label reasonUp to 500 characters; labeler up to 200.
contextAny JSON object up to 8 KB.
bodyAny 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.

statusrunning, then ready, or error when the report itself couldn't run (its error says why).
nHow many labelled cases the report froze when it started.
progress{ done, total }: cases regraded or failed so far.
productHow your product's own verdicts agree with the labels, or null when it logged none for this version.
regradeHow MacroEval's regrade with this version agrees with the labels.
perCaseOne 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.

failRecallOf the cases people failed, the share the grader also failed. Low means rejects slip through.
failPrecisionOf the cases the grader failed, the share people failed too. Low means false alarms.
agreementThe 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.json

Fetch 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.