Submit a batch of results to a run
const url = 'https://beta-api.plune.ai/v1/runs/example/results';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"results":[{"resultKey":"e2e/checkout.spec.ts::Checkout — empty cart::chromium","testCaseId":"6f1c2b3a-0000-4000-8000-0000000000c1","rawStatus":"passed","source":"playwright","execution":{"startedAt":"2026-09-16T09:12:04.000Z","finishedAt":"2026-09-16T09:12:05.310Z","durationMs":1310,"retry":0,"worker":"chromium-1"},"params":{"project":"chromium"}},{"resultKey":"e2e/checkout.spec.ts::Checkout — coupon applied::chromium","testCaseId":"6f1c2b3a-0000-4000-8000-0000000000c1","rawStatus":"failed","source":"playwright","execution":{"startedAt":"2026-09-16T09:12:05.400Z","finishedAt":"2026-09-16T09:12:09.021Z","durationMs":3621,"retry":1,"worker":"chromium-1"},"errorContext":"Error: expect(locator).toHaveText(\"−10 %\")\n Expected: \"−10 %\"\n Received: \"0 %\"\n at e2e/checkout.spec.ts:41:31","params":{"project":"firefox"},"failure":{"headline":"Error: expect(locator).toHaveText(\"−10 %\")","steps":["Pay","Apply the coupon"],"location":{"file":"e2e/checkout.spec.ts","line":41,"column":31},"artifacts":[{"name":"screenshot","contentType":"image/png"},{"name":"error-context","contentType":"text/markdown"},{"name":"trace","contentType":"application/zip"}],"ciUrl":"https://github.com/plune-ai/plune/actions/runs/35003177826"}}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://beta-api.plune.ai/v1/runs/example/results \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "results": [ { "resultKey": "e2e/checkout.spec.ts::Checkout — empty cart::chromium", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "rawStatus": "passed", "source": "playwright", "execution": { "startedAt": "2026-09-16T09:12:04.000Z", "finishedAt": "2026-09-16T09:12:05.310Z", "durationMs": 1310, "retry": 0, "worker": "chromium-1" }, "params": { "project": "chromium" } }, { "resultKey": "e2e/checkout.spec.ts::Checkout — coupon applied::chromium", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "rawStatus": "failed", "source": "playwright", "execution": { "startedAt": "2026-09-16T09:12:05.400Z", "finishedAt": "2026-09-16T09:12:09.021Z", "durationMs": 3621, "retry": 1, "worker": "chromium-1" }, "errorContext": "Error: expect(locator).toHaveText(\"−10 %\")\n Expected: \"−10 %\"\n Received: \"0 %\"\n at e2e/checkout.spec.ts:41:31", "params": { "project": "firefox" }, "failure": { "headline": "Error: expect(locator).toHaveText(\"−10 %\")", "steps": [ "Pay", "Apply the coupon" ], "location": { "file": "e2e/checkout.spec.ts", "line": 41, "column": 31 }, "artifacts": [ { "name": "screenshot", "contentType": "image/png" }, { "name": "error-context", "contentType": "text/markdown" }, { "name": "trace", "contentType": "application/zip" } ], "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826" } } ] }'Idempotent by resultKey, which is unique within the run. A repeated batch answers duplicate for every element and changes nothing; an element whose key is already held with DIFFERENT content answers conflict and names the result that holds it — a different failure included. Both are 200 with a per-element verdict — one bad element must not cost the other 499.
A failed attempt may carry failure (FailureDetailSubmission). A wrong value in it — a line that is not positive, a file named by a path of the machine, a link that is not http or https — refuses the WHOLE batch with 400, naming the element and the field, and nothing of it is stored. Size never refuses: the error text and every string of failure are stored without terminal colour codes, with credentials replaced by [redacted:<kind>] markers, and only then shortened (FailureDetail). The route takes up to 10485760 bytes, like POST /v1/runs, so a batch of long failures arrives whole.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Request Body required
Section titled “Request Body required ”object
object
Names the result WITHIN its run — the reporter chooses it, and sending it twice is how a retried upload stays free.
Optional. When sent it is stored as it is and the project’s status map is not consulted. Leave it out and send rawStatus with source: the project’s map, then the source’s defaults, decide (D4).
The runner’s own word, kept verbatim; status is what it maps to.
Which runner produced this — decides the mapping table used for rawStatus.
Defaults to passed. A failure that was expected is outcome: expected, not a regression.
object
Which attempt this was. Anything above 0 is what makes a pass flaky.
Never rejected for being large. Stored without terminal colour codes and with every credential replaced by a [redacted:<kind>] marker, then cut to 256 KB, head and tail.
Part of the signature: the same case with different params is a different test.
object
Reserved for per-file links, and never returned. The files a failed attempt kept travel in failure.artifacts.
object
object
A person’s marks per step of the case (D34) — source: manual. A reporter has no steps to mark.
object
The step’s position in the case’s steps, from 0.
What a reporter may say about a failed attempt, beside its error text. Every field is optional: a reporter sends the part it knows, and the run page shows the part that is there. No length or count is refused — the server sanitizes every string, then shortens (see FailureDetail). A wrong value is refused: a line that is not a positive integer, a file named by a path of the machine, a link that is not http or https.
object
The first line of the error the reporter chose — on a test timeout, the error of the action that was waiting. Absent or empty, and the page takes the first non-empty line of the text instead.
The declared steps (test.step) from the outermost to the one that failed. Absent when the test declares none.
Where the test failed: the first stack frame in the test file itself, or where the error was thrown when there is none.
object
From the repository root in posix form, the way a case’s specRef names it. A path of the machine is refused: absolute, with a drive or a scheme, from a home (~), or climbing out with ...
The files the runner kept for this attempt, by name — a name, not a link: the files live in the CI run ciUrl points to.
object
As the runner named it — screenshot, trace, error-context.
The CI run holding the artifacts. Absent outside CI: the artifacts are then named without a link.
Examples
A reporter’s batch — a pass, and a failure with its detail
{ "results": [ { "resultKey": "e2e/checkout.spec.ts::Checkout — empty cart::chromium", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "rawStatus": "passed", "source": "playwright", "execution": { "startedAt": "2026-09-16T09:12:04.000Z", "finishedAt": "2026-09-16T09:12:05.310Z", "durationMs": 1310, "retry": 0, "worker": "chromium-1" }, "params": { "project": "chromium" } }, { "resultKey": "e2e/checkout.spec.ts::Checkout — coupon applied::chromium", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "rawStatus": "failed", "source": "playwright", "execution": { "startedAt": "2026-09-16T09:12:05.400Z", "finishedAt": "2026-09-16T09:12:09.021Z", "durationMs": 3621, "retry": 1, "worker": "chromium-1" }, "errorContext": "Error: expect(locator).toHaveText(\"−10 %\")\n Expected: \"−10 %\"\n Received: \"0 %\"\n at e2e/checkout.spec.ts:41:31", "params": { "project": "firefox" }, "failure": { "headline": "Error: expect(locator).toHaveText(\"−10 %\")", "steps": [ "Pay", "Apply the coupon" ], "location": { "file": "e2e/checkout.spec.ts", "line": 41, "column": 31 }, "artifacts": [ { "name": "screenshot", "contentType": "image/png" }, { "name": "error-context", "contentType": "text/markdown" }, { "name": "trace", "contentType": "application/zip" } ], "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826" } } ]}A person’s mark (D34) — one case, step by step
{ "results": [ { "resultKey": "manual:6f1c2b3a-0000-4000-8000-0000000000c1", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "status": "failed", "source": "manual", "errorContext": "Step 3: the coupon field accepted an expired code.", "steps": [ { "index": 0, "status": "passed" }, { "index": 1, "status": "passed" }, { "index": 2, "status": "failed" } ] } ]}Responses
Section titled “ Responses ”Per-element verdicts and counts
object
One verdict per submitted element, in the order they were sent.
object
Position in the submitted batch.
Present when accepted — the result just created.
Present on duplicate and conflict — the result already holding the key.
Present when rejected.
object
Example
{ "items": [ { "index": 0, "resultKey": "e2e/checkout.spec.ts::Checkout — empty cart::chromium", "status": "accepted", "id": "6f1c2b3a-0000-4000-8000-0000000000e1" }, { "index": 1, "resultKey": "e2e/checkout.spec.ts::Checkout — coupon applied::chromium", "status": "accepted", "id": "6f1c2b3a-0000-4000-8000-0000000000e2" } ], "counts": { "accepted": 2, "duplicate": 0, "conflict": 0, "rejected": 0 }}Body is not a batch of submittable results — the whole batch is refused and nothing of it is stored
object
Examples
The line of the place is not a positive integer
{ "error": "validation failed — results.1.failure.location.line: Too small: expected number to be >0"}The file is a path of the machine, not from the repository root
{ "error": "validation failed — results.1.failure.location.file: must be a path from the repository root — no absolute path, drive, scheme, ~ or .."}The link to the artifacts is not a web address
{ "error": "validation failed — results.1.failure.ciUrl: must be an http or https address"}No / invalid token or session
Not found or not yours
object
Example generated
{ "error": "example"}Over 10485760 bytes — refused before anything reads the body
object
Example
{ "error": "request body too large — this route accepts at most 10485760 bytes"}Rate limited