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.
status | What happened |
|---|---|
accepted | Stored and queued for grading. digest is the sha256 of what was kept, so you can prove it. |
duplicate | The identical answer was already stored for this case — an idempotent retry, same digest. |
skipped | Nothing was stored and the case stays pending. |
rejected | Nothing 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