Sending JUnit XML test results from your CI
Sending JUnit XML test results to TestNod is a sequence of HTTP requests: one or more uploads using a shared build_id, followed by a single /finalize call once the build is done. This page is a high-level overview of that flow. For the exact request and response contract, see Submission API. For a working CI snippet, see CI integrations.
Supported test frameworks and formats
TestNod ingests JUnit XML files, which covers test frameworks like RSpec, pytest, JUnit, Jest (with jest-junit), Go test (with gotestsum), Cypress, Playwright, and most JVM runners. JUnit XML files are often generated with a single flag or a small reporter package. See JUnit XML format notes for which elements TestNod reads and which it ignores.
The upload and finalize endpoints
A complete build sends at least three requests. The first registers the upload and carries the metadata:
POST https://app.testnod.com/integrations/test_runs/upload
Project-Token: <your project token>
Content-Type: multipart/form-data
test_run[metadata][build_id]=<CI build id, required>
test_run[metadata][branch]=main
test_run[metadata][commit_sha]=<git sha>
test_run[metadata][run_url]=<CI run URL>
tags[][value]=smoke
That request does not carry the JUnit file. It returns a presigned_url, and you PUT the file straight to it:
PUT <presigned_url from the upload response>
Content-Type: application/xml
<your JUnit XML file>
Once every file for the build is uploaded, finalize the run:
POST https://app.testnod.com/integrations/test_runs/finalize
Project-Token: <your project token>
build_id=<CI build id>
The upload step can run more than once per build, with every call that shares a build_id attached to the same run. The finalize step finishes processing the test run. A successful upload returns 201 Created with test_run_id, upload_id, test_run_url, and the presigned_url to send the file to; finalize returns 200 OK with the run's current status. The full contract is in Submission API.
Finding the project token
Each project has its own token, generated when the project is created. Open the project in TestNod, click Settings, and copy the token. The token is the only credential the upload and finalize endpoints need. It scopes uploads to that project, so a leaked token only affects one project's data, but you should still treat it as sensitive.
What success looks like
Upload returns immediately with test_run_id and upload_id. The JUnit file is parsed asynchronously, and the parent run stays in pending until /finalize has been called and every upload is terminal. Once both are true, the run transitions to processing and then to processed.
What failure looks like
Failures fall into four buckets:
- 404 Not Found: the
Project-Tokenheader did not match any project (usually due to a typo or missing CI secret), or a/finalizecall named abuild_idwith no run behind it (usually means the finalize call and the upload did not have matchingbuild_idparameters). - 422 Unprocessable Content (upload): either
metadata.build_idwas missing or the request format was rejected. The response body has anerror_messagefield. - 422 Unprocessable Content (finalize): a run exists for that
build_idbut no uploads were ever attached to it. - Stuck in pending: the upload succeeded but no
/finalizecall ever landed for thatbuild_id. The run will not move until you call finalize (it is idempotent, so call it again from the samebuild_idwhenever you notice the stall).
The Troubleshooting uploads page walks through each one in order.