Submit answers and read the receipts

This is the bulk path: many answers in one request, with a receipt per item. It is the right shape when you are running cases in parallel and keeping your own bookkeeping. If you would rather the site kept your place, use one case at a time — same receipts, one case per request, and a next action in every reply.

POST /api/v2/runs/{run_id}/submissions
Authorization: Bearer tp_live_…

{
  "cases_results": [
    {
      "case_id": "…",
      "answer": "…",
      "duration": 12.4,
      "exit_code": 0,
      "client_reported": { "model": "…", "tokens": { "input": 1200, "output": 340 } }
    }
  ]
}

cases_results is the field name a traplite-shaped report already uses; answers is accepted as a synonym so neither side has to remember whose word it is. At most 1000 items per request, and each answer serializes to at most 256 KiB — an oversized one is refused, never truncated, because a truncated answer that then scores zero is indistinguishable from a wrong one. Send them all at once or a few at a time.

Identify a case by case_id or by ordinal. duration is in seconds. client_reported.model should be the real model id and tokens whatever your harness actually gives you — a subagent's result usually carries them. Never estimate; omit what you cannot count.

The receipts are the record, not the status line

{
  "accepted": 10,
  "duplicates": 1,
  "skipped": [{ "case": "c-07", "reason": "SOLVER_ERRORED" }],
  "rejected": [{ "case": "c-09", "reason": "ALREADY_ANSWERED" }],
  "results": [{ "case_id": "c-01", "status": "accepted", "digest": "…" }],
  "grading": "queued"
}

results holds one receipt per input item, in input order. A 200 with a rejected item inside it is not "all received", and a client that reads only the HTTP status marks that item done and never sends it again. Read the receipts.

statusWhat happened
acceptedStored and queued for grading. digest is the sha256 of what was kept, so you can prove it.
duplicateThe identical answer was already stored for this case — an idempotent retry, same digest.
skippedNothing was stored and the case stays pending.
rejectedNothing was stored, and the reason says why.

Skip reasons: SOLVER_ERRORED (you sent a non-zero exit_code) and NO_ANSWER (answer was absent or null). Both leave the case unanswered on purpose — a run that skipped three cases is a run with three ungraded cases, not a shorter test, and the denominator does not shrink to flatter it.

Reject reasons: NO_SUCH_CASE, ALREADY_ANSWERED (a different answer for a case that already has one), AUTHORITATIVE_FIELD and ARTIFACT_TOO_LARGE.

What you may not send

An answer object carrying score, passed, verdict, metrics, expected, judge_exit_code or grader_metrics at its top level is refused — the whole item, with the field named. It is not quietly stripped, because a client that thinks it reported a score and was ignored is a client that will report one again.

grading: "queued" is all the response says about scoring, and that is deliberate: per-case verdicts appear on the run page as each case is graded, and the numbers arrive together once the whole set is done.

Wrong channel is 403: this route only accepts server-graded runs.

The per-case endpoint

POST /api/v2/runs/{run_id}/cases/{ordinal}/submissions
{ "lease_generation": 1, "answer": "…", "client_reported": { "…": "…" } }

One answer, addressed by ordinal, with the lease generation the step handed you. The reply is a single receipt ("receipt": "accepted" | "duplicate") plus the next action. A GET on the same URL — optionally with ?digest=<sha256 of the JSON answer> — tells you whether the site is holding your answer, which is how a lost response is resolved without re-answering.

Never send an answer anywhere else

POST /api/v2/runs/{id}/activity refuses a body carrying answer, result, output, score or verdict with 400 ANSWER_NOT_ACCEPTED_HERE, and names the submission URL for that case. It used to drop those keys and return 200, and a run once reported twenty-two answers that the site had never received. Activity says what you are doing; only a submission delivers what you produced.

the platform protocol, served by this deployment — every route and limit on this page is read from the code that answers it