Documentation

TestNod documentation

Learn how to set up TestNod and configure it to help you spot flaky tests, catch regressions, and see how performance changes over time.

Submission API for uploading JUnit XML from CI

TestNod's Submission API is the set of HTTP endpoints your CI calls to send JUnit XML reports and create test runs. This page walks through each endpoint with worked examples, so you can wire it up by hand or understand what a CI integration is doing under the hood.

A complete build sends one or more uploads followed by a single finalize call:

upload → upload → upload → finalize

Every upload that shares a build_id attaches to the same run, and the finalize call is what moves that run out of pending. Without it, the test run never finishes processing and stays in a pending state indefinitely. The sequence is the same whether your build produces one JUnit file or twenty.

Endpoints

POST /integrations/test_runs/upload
POST /integrations/test_runs/finalize
POST /integrations/test_runs/upload_failed

Base URL: https://app.testnod.com. All three authenticate with the Project-Token header.

Upload

Content type: multipart/form-data.

Required fields

Field Description
Project-Token (header) The project token. See Authentication.
test_run[metadata][build_id] CI build identifier. Groups multi-file uploads into one run, and is the key the finalize call uses.

Optional fields

Field Type Notes
test_run[metadata][branch] string Branch the build ran on. Used by the baseline finder and run filters.
test_run[metadata][commit_sha] string Full commit SHA. Shown on the run page; used for cross-linking to your VCS.
test_run[metadata][run_url] string URL of the CI run that produced this report. Linked from the run page.
tags[][value] string (repeated) One or more tag values. If omitted, TestNod adds a single Default tag.

Successful response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": 12345,
  "project_id": "b7e2c1a0-9f3d-4a8b-bc1e-2d4f6a8c0e11",
  "test_run_id": 42,
  "upload_id": 1,
  "test_run_url": "https://app.testnod.com/projects/b7e2c1a0-9f3d-4a8b-bc1e-2d4f6a8c0e11/test_runs/42",
  "presigned_url": "https://..."
}

Your CI client PUTs the JUnit file directly to presigned_url. If that PUT fails, your CI client calls /integrations/test_runs/upload_failed with the test_run_id and upload_id from this response so the upload can be marked terminal.

Error responses

Status Body Meaning
404 Not Found {"error_message":"Project not found."} The Project-Token header is missing, malformed, or does not match any project.
422 Unprocessable Content {"error_message":"metadata.build_id is required."} test_run[metadata][build_id] was missing or blank.
422 Unprocessable Content {"error_message":"There was a problem creating this test run."} The request reached the project but TestNod could not save the run.
409 Conflict {"error_message":"Conflicting concurrent upload."} Two concurrent uploads for the same build_id raced. Retry once.

A 4xx response means TestNod did not record an upload. Retry the request after fixing the cause.

Sending the file to the presigned URL

The upload call registers the upload and hands back a presigned_url; the JUnit file itself is not part of that request. PUT the file straight to the returned URL, which is valid for five minutes:

curl -sS -H "Content-Type: application/xml" \
  --upload-file tmp/rspec.xml "$PRESIGNED_URL"

A successful PUT returns 200 with an empty body. If it fails, report it with the upload-failed endpoint so the upload can be marked terminal.

Finalize

After uploading every file for a build, call:

curl -sS -X POST https://app.testnod.com/integrations/test_runs/finalize \
  -H "Project-Token: $TESTNOD_PROJECT_TOKEN" \
  -d "build_id=$BUILD_ID"

build_id is a top-level form field, not nested under test_run[metadata]. Finalize is idempotent, so it is safe to retry on transient errors or call from more than one CI job. If every upload has reached a terminal state by the time finalize is called, the run transitions immediately; otherwise the worker transitions it when the last upload completes.

Response on success:

HTTP/1.1 200 OK
Content-Type: application/json

{ "status": "processing" }

The status is whatever state the run is in after the call: pending, processing, processed, or failed.

Error responses:

Status Body Meaning
404 Not Found {"error_message":"Test run not found for this build_id."} No run exists for (project, build_id). Confirm the upload step succeeded with the same build_id.
422 Unprocessable Content {"error_message":"build_id is required."} The build_id parameter was missing.
422 Unprocessable Content {"error_message":"No uploads received for this test run."} The run exists but no upload attached. Finalize will not transition an empty run.

Reporting an upload failure from your CI client

If your CI client's PUT to presigned_url fails, call:

curl -sS -X POST https://app.testnod.com/integrations/test_runs/upload_failed \
  -H "Project-Token: $TESTNOD_PROJECT_TOKEN" \
  -d "test_run_id=$TEST_RUN_ID" \
  -d "upload_id=$UPLOAD_ID" \
  -d "failure_message=s3 PUT returned 502"

This is for client-side failures only. Parsing errors are reported by TestNod after the upload completes, not from your CI client. Response is {"status":"failed"} with 200, or {"status": "<prior_status>"} if the upload was already terminal.

Async processing

The upload call returns immediately. The JUnit file is parsed asynchronously, and the parent run only moves out of pending once finalize has been called and every upload is terminal.

Worked example

A typical build sends one or more uploads and one finalize:

# 1. Register the upload. The response includes a presigned URL.
response=$(curl -sS -X POST https://app.testnod.com/integrations/test_runs/upload \
  -H "Project-Token: $TESTNOD_PROJECT_TOKEN" \
  -F "test_run[metadata][build_id]=$CI_BUILD_ID" \
  -F "test_run[metadata][branch]=$CI_BRANCH" \
  -F "test_run[metadata][commit_sha]=$CI_COMMIT_SHA" \
  -F "test_run[metadata][run_url]=$CI_RUN_URL" \
  -F "tags[][value]=smoke")

# 2. PUT the JUnit file to the presigned URL from that response.
upload_url=$(echo "$response" | grep -o '"presigned_url":"[^"]*"' | cut -d'"' -f4)
curl -sS -H "Content-Type: application/xml" --upload-file tmp/rspec.xml "$upload_url"

# 3. When every file for this build has been uploaded:
curl -sS -X POST https://app.testnod.com/integrations/test_runs/finalize \
  -H "Project-Token: $TESTNOD_PROJECT_TOKEN" \
  -d "build_id=$CI_BUILD_ID"

The CI integration pages under CI integrations wrap this same pair of calls into provider-specific syntax, including patterns for parallel and matrix builds.

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.