Documentation

Test runs API for JUnit XML results

Test runs and everything under them are read-only over the API. The JUnit XML your CI uploads arrives through the submission API and is never modified afterward, so these endpoints are for pulling results into dashboards, scripts, and reports. Authentication, pagination, and error codes are covered in the API overview.

Each project contains test runs, each test run contains test suites, and each test suite contains test cases. The endpoint URLs follow the same nesting.

The test run object

Field Type Notes
id integer Sequential per project, matching the run number in the web app.
project_id string The project the run belongs to.
build_id string The CI build identifier that grouped the uploads.
status string pending, processing, processed, or failed. Processing state, not test outcome.
result string or null tests_passed, tests_failed, or tests_errored. Null until the run is processed.
totals object tests, failures, errors, and skipped counts.
duration number Total run time in seconds.
branch string or null From the run metadata.
commit_sha string or null From the run metadata.
ci_run_url string or null Link back to the CI build.
tags array of strings Every tag on the run.
test_suites_count integer Number of suites in the run.
started_at string When the run was created.
url string The run's page in the web app.

status and result answer different questions. status tracks how far TestNod has gotten with processing the report, moving from pending through processing to processed, or to failed when the report could not be read. result is the test outcome, and it stays null until the run reaches processed. A run can therefore be processed and still have failing tests, which comes back as result: "tests_failed".

If you already handle the test_run.completed webhook, most of this will look familiar, since the two carry the same run fields under the same names. They are not interchangeable: the webhook wraps its fields in an event envelope and leaves out project_id and test_suites_count.

List test runs

GET /api/v1/projects/:project_id/test_runs

Newest first, with the standard limit and cursor parameters plus these filters:

Parameter Notes
status One of pending, processing, processed, failed.
result One of tests_passed, tests_failed, tests_errored.
build_id Exact match. Returns the run for one CI build.
tag Runs carrying this tag. The response still lists all of a matched run's tags.

An unrecognized value for status or result returns 400 rather than being ignored, so a typo surfaces instead of quietly returning everything.

curl -H "Authorization: Bearer $TESTNOD_API_KEY" \
  "https://testnod.com/api/v1/projects/3f1a2b7c-.../test_runs?result=tests_failed&limit=1"
{
  "data": [
    {
      "id": 482,
      "project_id": "3f1a2b7c-...",
      "build_id": "gh-run-9912",
      "status": "processed",
      "result": "tests_failed",
      "totals": { "tests": 1284, "failures": 3, "errors": 0, "skipped": 12 },
      "duration": 184.52,
      "branch": "main",
      "commit_sha": "9f2c1ab...",
      "ci_run_url": "https://github.com/acme/core/actions/runs/9912",
      "tags": [ "unit", "linux" ],
      "test_suites_count": 47,
      "started_at": "2026-08-30T09:09:02Z",
      "url": "https://testnod.com/projects/3f1a2b7c-.../test_runs/482"
    }
  ],
  "pagination": { "next_cursor": "cHVibGljX2lkOjQ4Mg", "has_more": true }
}

Get a test run

GET /api/v1/projects/:project_id/test_runs/:id

Run ids are sequential within a project rather than global. As an example, run 482 in one project has nothing to do with run 482 in another, so always pair a run id with the project it came from. A run id that the project has never issued returns 404.

Test suites

GET /api/v1/projects/:project_id/test_runs/:test_run_id/test_suites lists a run's suites in ascending id order, and GET .../test_suites/:id returns one.

Field Type Notes
id integer Sequential within the run.
test_run_id integer The parent run.
name string Suite name from the JUnit XML.
timestamp string or null The suite's own timestamp, when the XML supplied one.
totals object tests, failures, errors, and skipped counts.
duration number Suite time in seconds.
url string The suite's page in the web app.

Test cases

GET .../test_suites/:test_suite_id/test_cases lists a suite's cases in ascending id order, and GET .../test_cases/:id returns one.

Field Type Notes
id integer Unique across all test cases, not restarted per suite.
test_suite_id integer The parent suite.
name string Test name.
classname string Class or file the test came from.
status string passed, failed, errored, or skipped.
run_time number or null Seconds.
metadata object Failure, error, and skip details when present.
created_at string ISO 8601 UTC.
url string The case's page in the web app.

metadata carries whatever the JUnit XML supplied for that case. Failed cases usually include failure_message, failure_type, and failure_details, errored cases use the matching error_* keys, and skipped cases carry skipped_message when the report has one. It all comes straight from your test output, so treat it as untrusted text and escape it before rendering it anywhere.

Test cases are listed per suite rather than per run. To read every case in a large run, list its suites and page through each suite's cases with a cursor.

Be first to try TestNod

We're opening early access soon. Drop your email and we'll get you in, and we're happy to help you set up too.

No spam. We'll only email you about TestNod.