Skip to main content

CI/CD

Written by Ines

Overview

The Thunders CI API lets you queue test executions, poll their status, and retrieve structured reports (JUnit XML or JSON), all through 3 endpoints under /api/ci/.

When you test a Mobile App, the same pipeline can also upload each new build before it queues the runs. See Upload a Mobile App build below.

API Endpoints

POST  https://api.thunders.ai/api/ci/run
GET https://api.thunders.ai/api/ci/run/{runId}
GET https://api.thunders.ai/api/ci/run/{runId}/report

Authentication

The API requires authentication using a Bearer token:

Authorization: Bearer YOUR_THUNDER_TEST_TOKEN

You must obtain a valid API token from the Thunders platform. This token should be stored securely as a secret in your environment or GitHub repository.

Where to find your API token

API tokens belong to the organization rather than to a single project, so one token works across every project in it. You need an admin role to create one.

  1. Select your organization name at the top of the left sidebar to open the organization menu.

  2. Select Settings.

  3. Select API Tokens. The Organization API Tokens page lists every token with its status, when it was created and who created it.

  4. Create a new token, and confirm when asked "Are you sure you want to create a new API token?".

  5. Select Show to reveal the new token, then Copy.

  6. Paste it straight into your secret store. Treat it like a password, because anyone holding it can queue runs against your organization.

To withdraw a token, select Revoke on its row. Revoking cannot be undone, and any pipeline still using that token starts receiving 401 Unauthorized, so issue the replacement first.

Headers

Header

Value

Description

Authorization

Bearer YOUR_THUNDER_TEST_TOKEN

Authentication token

X-MS-API-ROLE

M2M

Indicates a machine-to-machine API call

Content-Type

application/json

Specifies the request body format

POST /api/ci/run — Queue Test Runs

Queues test executions for one or more test cases and/or test sets. All runs are grouped under a shared runId. Returns URLs you can use to poll status and fetch the final report.

Request Body

At least one of testCaseIds or testSetIds must be provided. Both can be provided in the same request and in that case both will be run.

Example

{
"projectId": "c8e34ec4-2464-43c7-8db8-1b3a47a22337",
"testCaseIds": ["a836fadc-377a-46fe-96b0-21f37c626bf9"],
"testSetIds": ["8eea2fd3-da6d-4434-a310-176be84c5646"],
"environmentId": "eca24252-e566-40a8-b2b0-707b7efa85d8",
"personaId": "70d0ac52-fe94-4d89-aba9-8ef28dd8c04c",
"maxParallelism": 5,
"maxRetries": 1,
"browserSettings": { "location": "Paris", "resolution": "1440x900", "deviceType": "Desktop" }
}

Parameters

Parameter

Type

Required

Default

Description

projectId

string (UUID)

Yes

-

The unique identifier of the project.

testCaseIds

array (UUIDs)

No

-

List of test case IDs to run.

testSetIds

array (UUIDs)

No

-

List of test set IDs. Each set expands to its test cases.

environmentId

string (UUID)

Yes

-

The environment to use.

personaId

string (UUID)

Yes

-

The persona for test execution.

maxParallelism

int

No

4

Max parallel tests. 0 = maximum.

maxRetries

int

No

0

Retry attempts for failed tests (0-3).

executionMode

string

No

parallel

parallel or sequential. In sequential, units run one at a time in alphabetical order of their names.

failureBehavior

string

No

continue

continue or stop. Only applies when executionMode = sequential. stop cancels the remaining runs after the first test case that ends in failed, once its retries are exhausted.

branchId

string (UUID)

No

main

The Branch whose tests run. See Run the tests of a Branch below.

testAssetReferences

array

No

-

Test asset refs for batch execution.

browserSettings.location

string

No

"Paris"

Geo cluster: Paris, London, Amsterdam, SanFrancisco, NewYork.

browserSettings.browserType

string

No

"Chromium"

Chromium, Chrome, Firefox, or Safari.

browserSettings.deviceType

string

No

"Desktop"

Desktop, Mobile, or Tablet.

browserSettings.resolution

string

No

"1280x720"

Viewport resolution (e.g., "1440x900").

Response — 202 Accepted

{
"runId": "01966a3e-...",
"statusUrl": "/api/ci/run/01966a3e-...",
"reportUrl": "/api/ci/run/01966a3e-.../report"
}

GET /api/ci/run/{runId} — Poll Run Status

Returns aggregated status of all test runs. Poll until status is terminal (passed, failed, stopped, cancelled).

Response — 200 OK

{
"runId": "01966a3e-...",
"status": "running",
"testCount": 5,
"passedCount": 2,
"failedCount": 0,
"runningCount": 2,
"queuedCount": 1,
"startedAt": "2026-03-20T14:30:00Z",
"endedAt": null,
"durationMs": null
}

GET /api/ci/run/{runId}/report — Fetch Report

Returns the test execution report. Format depends on the Accept header:

  • Default (or application/xml): JUnit XML, compatible with Jenkins, GitHub Actions (dorny/test-reporter), GitLab, and others.

  • Accept: application/json: JSON report with structured data.

Run the tests of a Branch

A CI run uses the tests on main unless you name another Branch. To run the version of the tests on a Branch, add branchId to the request body:

{
"projectId": "c8e34ec4-2464-43c7-8db8-1b3a47a22337",
"testSetIds": ["8eea2fd3-da6d-4434-a310-176be84c5646"],
"environmentId": "eca24252-e566-40a8-b2b0-707b7efa85d8",
"personaId": "70d0ac52-fe94-4d89-aba9-8ef28dd8c04c",
"branchId": "0199b2c4-7e1a-7c3d-9f20-5a6b8c9d0e1f"
}
  • The run uses the steps and settings that the Test Cases and Test Sets have on that Branch.

  • The statusUrl and reportUrl in the response carry the same branchId. Use them as the response returns them.

  • The API refuses a Branch that is merged, discarded or in another Test Project, with 400 Bad Request. It never runs main in its place.

  • Without branchId, the run uses main, so your existing pipelines do not change.

To find the id of a Branch, list the active Branches of the Test Project with the same token and headers:

GET   https://api.thunders.ai/api/branches?projectId={projectId}

Each Branch in the response carries its id and its name. main is always in the list.

To create a Branch and merge it in Thunders, see How do I change my tests on a Branch and merge them into main?

Error Handling

The API uses standard HTTP status codes:

  • 202 Accepted: POST /api/ci/run — tests were successfully queued

  • 200 OK: GET endpoints — request was successful

  • 400 Bad Request: Malformed request or missing required parameters

  • 401 Unauthorized: Authentication failed or token is invalid

  • 403 Forbidden: Insufficient permissions

  • 404 Not Found: No test runs found for the given runId

  • 500 Internal Server Error: Unexpected server error

Using with GitHub Actions

Integrate the Thunders CI API with GitHub Actions to automate test execution in your CI/CD pipeline.

Example GitHub Action Workflow

name: Run Thunders Tests
on:
workflow_dispatch:
permissions:
contents: read
checks: write
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Queue Thunders CI Run
id: trigger
run: |
set -euo pipefail
RESP=$(curl -sS --fail-with-body -X POST https://api.thunders.ai/api/ci/run \
-H "Authorization: Bearer ${{ secrets.THUNDER_TEST_TOKEN }}" \
-H "X-MS-API-ROLE: M2M" \
-H "Content-Type: application/json" \
-d '{"projectId":"YOUR_PROJECT_ID","testSetIds":["YOUR_TEST_SET_ID"],"environmentId":"YOUR_ENVIRONMENT_ID","personaId":"YOUR_PERSONA_ID","maxParallelism":5,"maxRetries":1}')
echo "status_url=$(echo "$RESP" | jq -r '.statusUrl')" >> $GITHUB_OUTPUT
echo "report_url=$(echo "$RESP" | jq -r '.reportUrl')" >> $GITHUB_OUTPUT
- name: Wait for completion
id: run-tests
run: |
set -euo pipefail
MAX_WAIT=5400; INTERVAL=30; ELAPSED=0
while [ $ELAPSED -lt $MAX_WAIT ]; do
RESP=$(curl -sS --fail-with-body \
"https://api.thunders.ai${{ steps.trigger.outputs.status_url }}" \
-H "Authorization: Bearer ${{ secrets.THUNDER_TEST_TOKEN }}" \
-H "X-MS-API-ROLE: M2M")
STATUS=$(echo "$RESP" | jq -r '.status')
echo "[$ELAPSED s] status=$STATUS"
case "$STATUS" in
passed|failed|stopped|cancelled)
echo "final_status=$STATUS" >> $GITHUB_OUTPUT; break;;
esac
sleep $INTERVAL; ELAPSED=$((ELAPSED+INTERVAL))
done
- name: Download JUnit report
run: |
curl -sS --fail-with-body \
"https://api.thunders.ai${{ steps.trigger.outputs.report_url }}" \
-H "Authorization: Bearer ${{ secrets.THUNDER_TEST_TOKEN }}" \
-H "X-MS-API-ROLE: M2M" \
-o test-results.xml
- name: Publish test results
uses: dorny/test-reporter@v1
continue-on-error: true
with:
name: Thunders CI Results
path: test-results.xml
reporter: java-junit
- name: Fail if tests did not pass
if: steps.run-tests.outputs.final_status != 'passed'
run: exit 1

Setting Up GitHub Secrets

  1. Go to your GitHub repository

  2. Navigate to Settings, then Secrets and variables, then Actions

  3. Click "New repository secret"

  4. Name: THUNDER_TEST_TOKEN

  5. Value: Your Thunders API token

  6. Click "Add secret"

Test Asset Batch

The Test Asset Batch feature allows you to run the same test case multiple times with different data variations by uploading CSV files as test assets. Each row in the CSV becomes a separate test run, enabling data-driven testing at scale.

How It Works

  1. Upload CSV files as test assets to your project (via the Thunders web app or API)

  2. Reference the CSV files in your API request using the testAssetReferences parameter

  3. Each CSV row generates a separate test run with its own variable overrides and browser settings

  4. All runs from the same batch are grouped under a shared batchId

With executionMode: sequential, each CSV row runs as an independent chain: the test cases within a row run one at a time in alphabetical order, while rows run in parallel. maxParallelism caps how many rows run at once. failureBehavior: stop is scoped to a single row.

The testAssetReferences Parameter

Add the testAssetReferences field to your request body:

{
"projectId": "your-project-id",
"testCaseIds": ["your-test-case-id"],
"environmentId": "your-environment-id",
"personaId": "your-persona-id",
"testAssetReferences": ["FILE_MYDATA_CSV", "FILE_USERS_CSV"]
}

Reference format: When you upload a file named mydata.csv, its reference becomes FILE_MYDATA_CSV. The naming convention is FILE_<FILENAME>_<EXT> where the filename is uppercased, and special characters are replaced with underscores.

To know more about the batching feature check out this documentation

Combining Multiple CSV Files

You can pass multiple CSV references in the testAssetReferences array. Rows from all files are merged sequentially into a single batch with continuous indexing.

If browsers.csv has 3 rows and mobile.csv has 2 rows, the batch will contain 5 total runs indexed 1 through 5.

Upload a Mobile App build

When you test a Mobile App, your pipeline can send each new APK or IPA to Thunders before it queues the runs. The upload replaces the build of that App in one Environment, and the next run installs it. A run that has already started finishes on the build it started with.

GET   https://api.thunders.ai/api/projects/{projectId}/apps
POST https://api.thunders.ai/api/apps/{appId}/builds

Both endpoints use the same API token and the same Authorization and X-MS-API-ROLE: M2M headers as the CI endpoints above.

Find the App and Environment IDs

GET /api/projects/{projectId}/apps lists the Apps of a Test Project. Each App carries its id and an environments array, with the environmentId and environmentName of each Environment and the build it currently holds. Copy the two ids into your pipeline once.

Upload the build

POST /api/apps/{appId}/builds takes a multipart/form-data body instead of JSON.

Field

Type

Required

Description

projectId

string (UUID)

Yes

The Test Project that holds the App.

environmentId

string (UUID)

Yes

The Environment whose build is replaced.

file

file

Yes

An .apk or .ipa file, 500 MB at most.

Response, 200 OK

{
"fileName": "app-release.apk",
"fileSizeBytes": 14200000,
"platform": "android",
"source": "ci",
"uploadedAt": "2026-10-13T09:30:00Z",
"appPackage": "com.acme.shop",
"appVersion": "2.4.1",
"appBuildNumber": "241"
}

Field

Type

Description

platform

string

android or ios, from the file extension.

source

string

ci for a build uploaded with an API token. Thunders shows a CI badge on that build.

appPackage

string

The Android package name or the iOS bundle id, read from the file.

appVersion

string?

versionName on Android, CFBundleShortVersionString on iOS. null when the build does not declare one.

appBuildNumber

string?

versionCode on Android, CFBundleVersion on iOS. null when the build does not declare one.

Every Test Run on a Mobile App records the version and the build number of the build it installed, and its report shows them in the App build row.

Errors

  • 400 Bad Request: no file, an empty file, a file that is not .apk or .ipa, or a build whose package name cannot be read

  • 401 Unauthorized: the token is missing or invalid

  • 404 Not Found: the App or the Environment does not exist in that Test Project, or belongs to another organization

  • 413 Payload Too Large: the file is larger than 500 MB

Example GitHub Actions step

Add this step before the step that queues the runs:

      - name: Upload the new build to Thunders
run: |
curl --fail-with-body -sS -X POST \
"https://api.thunders.ai/api/apps/YOUR_APP_ID/builds" \
-H "Authorization: Bearer ${{ secrets.THUNDER_TEST_TOKEN }}" \
-H "X-MS-API-ROLE: M2M" \
-F "projectId=YOUR_PROJECT_ID" \
-F "environmentId=YOUR_ENVIRONMENT_ID" \
-F "file=@app/build/outputs/apk/release/app-release.apk"

--fail-with-body stops the job on an error and prints the message that explains it.

When you do not need a pipeline

If all you want is for a Test Set to run on a recurring rule, a Schedule does that inside Thunders, with no pipeline to maintain and no token to rotate. Open the Test Set, select the Schedules tab, and pick one of five frequencies from Hourly to Monthly.

Keep using CI when the run has to happen at a point in your delivery process, such as after a deployment or on a pull request, or when you need a run more often than once an hour. Hourly is the shortest interval a Schedule supports.

Did this answer your question?