Import CI test results with the API

Import JUnit XML, Katalon Studio reports, or Katalon Recorder logs from a CI job with tenant-host v1 routes.

Before you start

Send requests to your tenant host, https://<tenant>.katalon.io, with no
trailing slash. Use HTTP Basic with an empty username and your Katalon API
key as the password. Keep the key in your CI secret store. Do not turn on shell
tracing (set -x).

Pass projectId in each request's query string. These routes do not use the v2
x-project-id header. Your API-key user must be allowed to upload reports to
that project. The key does not grant access to every project.

The import routes do not return TEST_RUN_ID. For the ID definition, see
Know which ID is which.

Set TENANT_HOST, PROJECT_ID, and KATALON_API_KEY from your CI variables
and secrets. Set CI_RUN_ID to a globally unique identifier for each CI run.
Set TENANT_HOST to your tenant URL without a trailing slash. The
helper sends the API key through standard input, not a curl process argument.
If your key contains " or \, escape them as \" and \\ in curl's
configuration format.

Use Bash, curl 7.87 or later, and jq for these examples.

set -euo pipefail
umask 077

katalon_request() {
  : "${KATALON_API_KEY:?Set API key}"
  printf 'user = ":%s"\n' "${KATALON_API_KEY}" |
    curl --config - --silent --show-error --fail-with-body "$@"
}

1. Allocate one upload destination per file

Call the existing Allocate file upload URLs operation with numberUrl=1 for each report file. Send projectId and numberUrl in the query string.

DESTINATION_FILE=$(mktemp)
katalon_request --get \
  --url-query "projectId=${PROJECT_ID:?Set project ID}" \
  --url-query 'numberUrl=1' \
  --output "$DESTINATION_FILE" \
  "${TENANT_HOST:?Set tenant host}/api/v1/files/upload-urls"

UPLOADED_PATH=$(jq -er '.[0].path' "$DESTINATION_FILE")
UPLOAD_URL=$(jq -er '.[0].uploadUrl' "$DESTINATION_FILE")

The response is a JSON array. Keep DESTINATION_FILE, UPLOADED_PATH, and
UPLOAD_URL private. Do not print the signed URL or add it to shared logs. Use
the path value as uploadedPath when you register the file. Do not use the
local file path or the signed URL as uploadedPath.

2. Upload the file bytes

Use plain curl for the PUT. Do not use katalon_request, send the API key, or
follow a redirect with -L.

REPORT_FILE=${REPORT_FILE:?Set report file path}
printf 'url = "%s"\n' "${UPLOAD_URL}" |
  curl --config - --request PUT --upload-file "$REPORT_FILE" \
    --silent --show-error --fail-with-body

The signed URL is a private upload capability. Keep it out of logs and use it
only for this PUT. Register metadata only after the PUT succeeds.

Check the PUT status code before you register. If the file is not in storage,
registration fails with 500 and a storage "Not Found" message, not a 4xx
error.

3. Register the uploaded files

A batch is a client-chosen key that groups report files for one import.
For the fresh JUnit flow below, one completed batch becomes one test run.
Katalon folder and session reuse can change this relationship; see the Katalon
examples below. Use one log per Recorder batch.

Register JUnit files as one test run

Use one fresh batch value across all files in the import. Keep the same
project and Basic-auth credentials for every file. Choose a new batch value
across projects and report types. Set isEnd=true only on the final
registration, after every file PUT succeeds.

For a fresh JUnit import without sessionId, this two-file example groups both
XML files into one test run. An explicit sessionId can join batches to an
existing test run instead.

Allocate and PUT a separate destination for each XML file with steps 1 and 2.
Store each returned path and signed URL in private variables. Then register
the two files in order:

BATCH="ci-${CI_RUN_ID:?Set a globally unique CI run ID}-junit"
REPORT_FOLDER=${REPORT_FOLDER:?Set the JUnit report folder path}

katalon_request --request POST \
  --url-query "projectId=${PROJECT_ID}" \
  --url-query "batch=${BATCH}" \
  --url-query 'isEnd=false' \
  --url-query "fileName=$(basename -- "${JUNIT_XML_A:?Set first XML file}")" \
  --url-query "uploadedPath=${UPLOADED_PATH_A:?Set first returned path}" \
  --url-query "folderPath=${REPORT_FOLDER}" \
  "${TENANT_HOST}/api/v1/junit/test-reports"

katalon_request --request POST \
  --url-query "projectId=${PROJECT_ID}" \
  --url-query "batch=${BATCH}" \
  --url-query 'isEnd=true' \
  --url-query "fileName=$(basename -- "${JUNIT_XML_B:?Set second XML file}")" \
  --url-query "uploadedPath=${UPLOADED_PATH_B:?Set second returned path}" \
  --url-query "folderPath=${REPORT_FOLDER}" \
  "${TENANT_HOST}/api/v1/junit/test-reports"

The JUnit route requires projectId, batch, isEnd, fileName,
uploadedPath, and folderPath. projectId is a decimal string. isEnd is
the string true or false. sessionId and pushToXray are optional strings.
If you omit sessionId, the server creates a unique session. If you send one,
keep it consistent on the final registration. Use whitespace-free file names
that end in .xml. The parser reads a <testsuite> or <testsuites> root;
it does not import a ZIP in place of those XML files.

The route returns 204 with no body. It does not return TEST_RUN_ID, and
successful registration does not confirm that parsing finished.

Register Katalon Studio files

Preserve the Studio report folder structure in folderPath. Include each
eligible report folder's execution0.log and its related files. Register each
uploaded file with the single-file route. Use the same batch for the files,
and send isEnd=true only on the last file.

KATALON_BATCH="ci-${CI_RUN_ID:?Set a globally unique CI run ID}-katalon-single"
katalon_request --request POST \
  --url-query "projectId=${PROJECT_ID}" \
  --url-query "batch=${KATALON_BATCH}" \
  --url-query 'isEnd=true' \
  --url-query 'fileName=execution0.log' \
  --url-query "uploadedPath=${UPLOADED_PATH:?Set returned path}" \
  --url-query "folderPath=${STUDIO_FOLDER:?Set original report folder}" \
  "${TENANT_HOST}/api/v1/katalon/test-reports"

The single-file Katalon route requires the same six query parameters as JUnit.
It does not accept JUnit's optional sessionId. pushToXray is optional. The 200
response is an array of upload-batch resources. Its id is an upload-batch ID,
not TEST_RUN_ID. webUrl opens the batch. The returned done value is
false; it does not show whether parsing finished.
The response can be [] before an end marker. The default Katalon cap is
3,500 file rows per batch. The server can store over-cap files but may not
start normal batch processing. This cap does not apply to direct JUnit or
Recorder imports.

You can also register several Katalon files in one JSON array. Use
POST /api/v1/katalon/test-reports/multiple when that request shape fits your
CI job. The query projectId is a number on this route. batchType is
optional and defaults to KATALON_STUDIO; do not use another enum value as a
replacement for the JUnit or Recorder routes.

[
  {
    "folderPath": "reports/smoke",
    "fileName": "execution0.log",
    "uploadedPath": "<path returned for execution0.log>",
    "end": false
  },
  {
    "folderPath": "reports/smoke",
    "fileName": "execution.properties",
    "uploadedPath": "<path returned for execution.properties>",
    "end": true
  }
]

Save that array as katalon-files.json after you PUT every listed file. The
folderPath, fileName, and uploadedPath fields are optional, but supply
all three for each file. Use end, not isEnd, in the
array. fullPath is optional and the server derives it from the folder and
file name. sessionId, if sent, looks up an existing test run; it does not
create a run with a new session value.

MULTIPLE_BATCH="ci-${CI_RUN_ID:?Set a globally unique CI run ID}-katalon-multiple"
katalon_request --request POST \
  --url-query "projectId=${PROJECT_ID}" \
  --url-query "batch=${MULTIPLE_BATCH}" \
  --url-query 'batchType=KATALON_STUDIO' \
  --header 'Content-Type: application/json' \
  --data @katalon-files.json \
  "${TENANT_HOST}/api/v1/katalon/test-reports/multiple"

Use --url-query for these POST query parameters. Do not use
--data-urlencode for them; that option can put values in the POST body.
Neither Katalon route guarantees that one batch creates one test run. A batch
can include multiple folders or sessions, and a run can span batches.

The multiple-file Katalon route returns the same upload-batch array. Its id
is not TEST_RUN_ID, and its done value is not a parsing-completion signal.
The default Katalon cap is 3,500 file rows per batch, not a byte limit.

Register a Katalon Recorder log

Use one Recorder execution log per fresh batch. The Recorder route accepts only
projectId, batch, isEnd, fileName, and uploadedPath; it does not
accept folderPath, sessionId, or pushToXray.

RECORDER_BATCH="ci-${CI_RUN_ID:?Set a globally unique CI run ID}-recorder"
katalon_request --request POST \
  --url-query "projectId=${PROJECT_ID}" \
  --url-query "batch=${RECORDER_BATCH:?Set a fresh batch value}" \
  --url-query 'isEnd=true' \
  --url-query "fileName=$(basename -- "${RECORDER_LOG:?Set Recorder log}")" \
  --url-query "uploadedPath=${RECORDER_PATH:?Set returned path}" \
  "${TENANT_HOST}/api/v1/katalon-recorder/test-reports"

The route returns 200 with no body. The parser reads the batch's first file,
not every file in the batch. Repeating the final request can reprocess or reset
the run linked to that file.

Check import outcomes and retries

Use pushToXray=true on JUnit or single-file Katalon requests to request an
Xray push. A value of false or omission does not guarantee that no push
occurs; the test run's policy can still allow one. The Recorder and multiple
Katalon routes do not accept this option.

Katalon can skip a repeated, completed upload when repeat-skip protection is
enabled and all stored folder, file name, and uploaded path keys match.
Queue deduplication can replace older work that has not started, but cannot
cancel active work. Neither protection guarantees exactly-once processing.
JUnit can create another run if you repeat the final
request without reusing a sessionId; Recorder can reprocess or reset its
first file's run. Check the project before you retry a timed-out registration.

The JUnit 204 and Recorder 200 responses have no body. Katalon returns an
upload-batch resource, not a test-run resource. All three import types process
reports asynchronously, so a successful registration does not confirm that
the report parser finished.

Use the Katalon Report Uploader

Use the Katalon Report Uploader CLI as a no-code alternative to sending API
requests. Set its server to --server=https://<tenant>.katalon.io and
set --type to junit, katalon, or katalon_recorder. Download a JAR
from the Katalon Report Uploader releases.

Remove the private allocation-response files after the import. Unset the signed
URL variables when you no longer need them.


Did this page help you?