Tour of the API
See how QualGent API objects (apps, test cases, missions) fit together so you can wire automated mobile testing directly into your CI/CD pipeline.
The QualGent API is powerful and flexible once you know how to use it. This tour covers key information to help you understand the API more deeply:
- The core objects we use across the API
- The path a mission takes, from app upload to results
- The objects that play a role and how to determine when they're needed
- Common patterns and best practices for combining them
Understanding these patterns helps you move beyond the pre-written snippets in our quickstarts. You can migrate ad-hoc integrations to more structured patterns, combine simple patterns in novel ways, and plan for future growth.
Core concepts
Everything is an object
Everything in your QualGent account is an object. Your uploaded builds correspond to App objects, and your library of reusable test scenarios are TestCase objects. A Category groups related test cases. A Submission is one request for QualGent to run a set of test cases against a build and review the results. A BugReport describes a bug found on a build: its steps, expected and actual results, severity, and evidence.
Objects have lives
Most objects progress through states. A Submission starts as received, moves through testing and verifying, and ends as complete. An App is uploaded once and then referenced by ID across many requests. Retaining these IDs in your CI scripts is the key to building reliable pipelines.
An integration is made out of cooperating objects
A typical CI integration uploads a new build, submits a mission request against that build, and polls the request until it completes. No single endpoint does all three — your integration is the choreography between Apps, Test cases, and Missions.
Key features
Test execution
Request runs for a set of test cases against an uploaded build.
CI/CD integration
Plug automated testing directly into your GitHub Actions, CircleCI, or Jenkins pipeline.
Progress tracking
Follow each request's stage, progress, and verified bugs until it completes.
Multi-platform support
Submit Android and iOS builds; the device follows from the build.
The path a mission takes
Here's what happens between the moment you POST a build and the moment verified results come back. A mission request is run and reviewed by QualGent, so the path is longer than a single API call. Knowing each stage makes polling, retries, and failures much easier to reason about.
1. Ingest & normalize the build
When a multipart request hits POST /v1/apps/upload, the API streams the file into memory while tracking size incrementally. The upload pipeline is strictly ordered:
- Receive — the binary is read off the wire as a stream.
- Chunk (large files only) — builds at or above 32 MB are split server-side into 8 MB segments and reassembled on the storage side. The caller only ever sees a single multipart request.
- Persist — the file is stored against your account. The file record and its underlying storage are written atomically — you'll never end up with a partial upload.
You get back the full file record. The id on that record is the app_file_id you'll reuse forever.
2. Submit a request
POST /v1/missions/submit takes up to 200 test case IDs and an optional app_file_id (your latest uploaded app is used when you leave it out). The request is recorded as a Submission with one entry per test case, and you get back a submission_id.
An organization has one open request at a time. If one is already open, the call returns 409 with code: "run_in_progress" and nothing is created. A test case that can't be submitted is reported in failures while the rest go ahead, so check failed even on a 200.
3. QualGent runs the test cases
QualGent executes each test case against your build. You don't choose a device; it follows from the build you submitted. The request's stage starts as received and moves to testing when the first test case starts.
A request can be cancelled only while nothing in it has started. After that it is QualGent's to finish.
4. Review & results
Every result is reviewed by QualGent before it is published to you. The request moves through two more stages:
verifying— every test case has a result and QualGent is reviewing them.complete— every result has been published.
Read the request with GET /v1/missions/submissions?submission_id=.... It carries progress as percentages, verified bugs, and any test cases QualGent could not cover in notCovered. A request can take hours, so poll on a long interval rather than in a tight loop.
Each entry in bugs is a summary. Read the full reports for the build with GET /v1/bug-reports?app_file_id=..., or one of them with GET /v1/bug-reports/{id}.
5. What's enforced along the way
Every hop in the path above enforces a few invariants you can rely on:
- Org scoping is total. An API key resolves to exactly one organization, and every query is filtered by it. No endpoint will ever return another org's apps, runs, or categories.
- Rate limits are 10 req/s per key, sliding window, with a
Retry-Afteron 429. Burst submission is fine; sustained polling should stay ≥ 15 second intervals. - Pre-signed download URLs (from
GET /v1/apps/{id}) are valid for 5 minutes. Treat them as one-shot.
Quickstart
A typical integration walks three steps:
Upload your application
Submit your mobile build via the Apps API. The endpoint handles large files transparently — no client-side chunking required.
Request runs
Submit a set of test cases against your upload via the Missions API.
Get results
Follow the request's stage and progress, then read the verified bugs when it completes.
List all available test cases in your organization with a single request. This is the lightest-weight call you can make — it's a good way to verify your key works before you upload a build.
curl https://api.qualgent.ai/v1/test-cases/list \
-H "X-Api-Key: qg_your_api_key_here"import requests
response = requests.get(
"https://api.qualgent.ai/v1/test-cases/list",
headers={"X-Api-Key": "qg_your_api_key_here"},
)
print(response.json())const response = await fetch("https://api.qualgent.ai/v1/test-cases/list", {
headers: { "X-Api-Key": "qg_your_api_key_here" },
});
console.log(await response.json());package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.qualgent.ai/v1/test-cases/list", nil)
req.Header.Add("X-Api-Key", "qg_your_api_key_here")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}[
{
"id": "tc_1234567890",
"name": "Login Flow Test",
"status": "active",
"category": { "id": "cat_abc123", "name": "Smoke Tests" }
}
]If everything worked, you'll get a JSON array of your organization's test cases. From here, head to the Requesting runs with missions guide to request runs.
Authentication
Authenticate requests by including your API key as the X-Api-Key header. Keys are organization-scoped — never commit them to source control.
Getting your API key
- Create an account at app.qualgent.ai/auth/sign-up.
- From the dashboard, open Settings → Developer to manage API keys.
- Generate a new key, copy it somewhere safe, or revoke stale keys.
Header format
| Header | Value | Description |
|---|---|---|
| X-Api-Key | qg_your_api_key | Your QualGent API key |
Storing keys securely
Use environment variables — never hardcode keys:
export QUALGENT_API_KEY="qg_your_api_key_here"
curl https://api.qualgent.ai/v1/apps/list \
-H "X-Api-Key: $QUALGENT_API_KEY"import os, requests
from dotenv import load_dotenv
load_dotenv()
response = requests.get(
"https://api.qualgent.ai/v1/apps/list",
headers={"X-Api-Key": os.environ["QUALGENT_API_KEY"]},
)require("dotenv").config();
const response = await fetch("https://api.qualgent.ai/v1/apps/list", {
headers: { "X-Api-Key": process.env.QUALGENT_API_KEY },
});Rate limits
Rate limits are applied per API key to ensure fair usage and system stability.
| Limit | Description |
|---|---|
| 10 req/sec | Maximum requests per second, per API key |
Every response includes usage headers:
X-RateLimit-Limit— maximum requests per secondX-RateLimit-Remaining— requests remaining in the current windowX-RateLimit-Reset— seconds until the window resets
Requests over the limit return 429 Too Many Requests with a Retry-After header indicating the back-off duration.
Guides
Task-oriented walkthroughs for the three most common things you'll do with the QualGent API — manage app builds, request runs, and follow their progress.
Managing your applications
Upload the mobile application you want to test, then reference it by ID in every subsequent request. Accepted formats: .apk, .ipa, .aab.
Uploading an app
curl https://api.qualgent.ai/v1/apps/upload \
-H "X-Api-Key: qg_your_api_key_here" \
-F "file=@/path/to/your/app.apk" \
-F "app_name=MyApp" \
-F "version=1.0.0"import requests
with open("/path/to/your/app.apk", "rb") as f:
response = requests.post(
"https://api.qualgent.ai/v1/apps/upload",
headers={"X-Api-Key": "qg_your_api_key_here"},
files={"file": f},
data={"app_name": "MyApp", "version": "1.0.0"},
)
app_file_id = response.json()["file"]["id"]import FormData from "form-data";
import fs from "fs";
const form = new FormData();
form.append("file", fs.createReadStream("/path/to/your/app.apk"));
form.append("app_name", "MyApp");
form.append("version", "1.0.0");
const response = await fetch("https://api.qualgent.ai/v1/apps/upload", {
method: "POST",
headers: { "X-Api-Key": "qg_your_api_key_here", ...form.getHeaders() },
body: form,
});
const { file } = await response.json();package main
import (
"bytes"
"mime/multipart"
"net/http"
"os"
"io"
)
func main() {
f, _ := os.Open("/path/to/your/app.apk")
defer f.Close()
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
part, _ := w.CreateFormFile("file", "app.apk")
io.Copy(part, f)
w.WriteField("app_name", "MyApp")
w.WriteField("version", "1.0.0")
w.Close()
req, _ := http.NewRequest("POST", "https://api.qualgent.ai/v1/apps/upload", body)
req.Header.Add("X-Api-Key", "qg_your_api_key_here")
req.Header.Set("Content-Type", w.FormDataContentType())
http.DefaultClient.Do(req)
}{
"success": true,
"file": {
"id": "3f9a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"file_path": "user123/1757339253852-MyApp-1.0.0.apk",
"filename": "1757339253852-MyApp-1.0.0.apk",
"original_name": "app.apk",
"file_size": 52428800,
"file_type": "application/vnd.android.package-archive",
"app_name": "MyApp",
"version": "1.0.0",
"os": "Android",
"package_name": "com.example.myapp",
"user_id": "6f88bc25-2f3e-44ec-9486-dadb388fd4f4",
"organization_id": "58ecde76-d294-43aa-934b-0142a506b4e9"
}
}Handling test cases
Test cases are the reusable scenarios QualGent runs against your builds. List them to find the IDs you submit in a mission request, inspect one in full, or create and update them through the API. To run them, see Requesting runs with missions.
List test cases
Returns every test case in your organization with its category, newest first. Pass category to return only the test cases in one category.
curl "https://api.qualgent.ai/v1/test-cases/list?category=770e8400-e29b-41d4-a716-446655440000" \
-H "X-Api-Key: qg_your_api_key_here"import requests
response = requests.get(
"https://api.qualgent.ai/v1/test-cases/list",
headers={"X-Api-Key": "qg_your_api_key_here"},
params={"category": "770e8400-e29b-41d4-a716-446655440000"},
)
for test_case in response.json():
print(test_case["id"], test_case["name"], test_case["status"])[
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Login Flow Test",
"status": "active",
"category": { "id": "770e8400-e29b-41d4-a716-446655440000", "name": "Smoke Tests" }
}
]An organization with no test cases gets an empty object, {}, rather than an empty array.
Get a test case
Returns one test case in full: its steps, expected_result, priority, category, declared variables, attached files, and its version history in versions.
curl https://api.qualgent.ai/v1/test-cases/660e8400-e29b-41d4-a716-446655440000 \
-H "X-Api-Key: qg_your_api_key_here"Create a test case
name, steps and expected_result are required. Each step is one action; kind is optional and marks it as setup, act or verify. You can also set priority, description, category_id and variables.
curl https://api.qualgent.ai/v1/test-cases \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Login Flow Test",
"steps": [
{ "description": "Open the app", "kind": "setup" },
{ "description": "Tap Log In and enter valid credentials", "kind": "act" },
{ "description": "Verify the home screen is shown", "kind": "verify" }
],
"expected_result": "The user lands on the home screen",
"priority": "High"
}'{
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Login Flow Test",
"version_id": "880e8400-e29b-41d4-a716-446655440000",
"files": [],
"warnings": null
}Update a test case
Send only the fields you want to change. Every update creates a new version, and the response returns its version_number and version_id.
curl -X PATCH https://api.qualgent.ai/v1/test-cases/660e8400-e29b-41d4-a716-446655440000 \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "priority": "Medium" }'Requesting runs with missions
A mission request hands a set of test cases and a build to QualGent. It follows the same flow as the Run button in the dashboard: the request is recorded as a Submission, QualGent executes the tests, reviews every result, and reports verified bugs back to you. You don't pick a device; it follows from the build you submit.
Submit a request
curl https://api.qualgent.ai/v1/missions/submit \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"test_case_ids": [
"660e8400-e29b-41d4-a716-446655440000",
"660e8400-e29b-41d4-a716-446655440001"
],
"app_file_id": "550e8400-e29b-41d4-a716-446655440000"
}'import requests
response = requests.post(
"https://api.qualgent.ai/v1/missions/submit",
headers={"X-Api-Key": "qg_your_api_key_here"},
json={
"test_case_ids": [
"660e8400-e29b-41d4-a716-446655440000",
"660e8400-e29b-41d4-a716-446655440001",
],
"app_file_id": "550e8400-e29b-41d4-a716-446655440000",
},
)
result = response.json()
print(result["submission_id"], result["submitted"], result["failed"]){
"submitted": 2,
"failed": 0,
"failures": [],
"submission_id": "7b0e8400-e29b-41d4-a716-446655440000"
}A test case that can't be submitted is listed in failures with the reason, and the rest still go ahead. The response is 200 either way, so check failed.
One open request at a time
An organization has one open request at a time. A request stays open until QualGent has published a result for every test case in it. While one is open, POST /v1/missions/submit returns 409 with code: "run_in_progress" and the open request in open_run.
Send "replace_open": true to cancel the open request and start the new one. Replacing removes the earlier request along with any results already recorded on it, so use it only when the earlier request is no longer wanted.
Follow progress
curl "https://api.qualgent.ai/v1/missions/submissions?submission_id=7b0e8400-e29b-41d4-a716-446655440000" \
-H "X-Api-Key: qg_your_api_key_here"Each request carries a stage:
| Stage | Meaning |
|---|---|
received | The request is recorded and nothing has started yet |
testing | At least one test case has started |
verifying | Every test case has a result and QualGent is reviewing them |
complete | Every result has been published |
progress gives overall, tested and verified as percentages, and minutesLeft estimates the time left in the current stage when it is known. Verified bugs appear in bugs with their id, bugId and summary, and test cases QualGent could not cover appear in notCovered with a note. A request can take hours, so poll on a long interval rather than in a tight loop. To get the steps, severity and evidence of each bug, see Reading bug reports.
Cancel a request
A request can be cancelled while nothing in it has started; canCancel on the request tells you whether that is still the case. After that, the call returns 409. A cancelled request is removed.
curl -X POST https://api.qualgent.ai/v1/missions/submissions/7b0e8400-e29b-41d4-a716-446655440000/cancel \
-H "X-Api-Key: qg_your_api_key_here"Reading bug reports
A BugReport is the full write-up of a bug: the steps to reproduce it, what was expected and what happened, its severity, and the screenshot or recording that shows it. The bugs on a mission request carry only an id, bugId and summary. This is where you read the rest.
Bug reports for a build
Pass a build's app_file_id to get every bug report filed against it, most severe first and newest first within a severity. This is a GET request: the build goes in the query string, your API key goes in the X-Api-Key header, and there is no request body.
app_file_id is the build's id, not its version number. It is returned when you upload the build, and GET /v1/apps/list lists the builds you already have.
curl "https://api.qualgent.ai/v1/bug-reports?app_file_id=550e8400-e29b-41d4-a716-446655440000" \
-H "X-Api-Key: qg_your_api_key_here"import requests
response = requests.get(
"https://api.qualgent.ai/v1/bug-reports",
headers={"X-Api-Key": "qg_your_api_key_here"},
params={"app_file_id": "550e8400-e29b-41d4-a716-446655440000"},
)
for bug in response.json()["bugs"]:
print(bug["bugId"], bug["severity"], bug["summary"]){
"bugs": [
{
"id": "9d0e8400-e29b-41d4-a716-446655440000",
"bugId": "BUG_014",
"summary": "Keyboard stays open after leaving the chat screen",
"steps": "Open the app\nOpen any chat\nTap the message field\nGo back to the chat list",
"resultDetails": {
"expected": "The keyboard closes when the chat screen is left.",
"actual": "The keyboard stays open over the chat list."
},
"status": "open",
"severity": "medium",
"failureClassification": "app_bug",
"appFileId": "550e8400-e29b-41d4-a716-446655440000",
"appFile": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"appName": "MyApp",
"version": "1.4.2",
"os": "android",
"originalName": "myapp-1.4.2.apk"
},
"attachment": {
"name": "keyboard-stays-open.mp4",
"type": "video/mp4",
"size": 4816220,
"downloadUrl": "https://..."
},
"externalTicket": {
"provider": "github",
"key": "acme/myapp#42",
"url": "https://github.com/acme/myapp/issues/42"
},
"createdAt": "2026-10-02T11:53:44.507649+00:00",
"updatedAt": "2026-10-02T12:55:22.176645+00:00"
}
]
}Narrow the result with status, severity and failure_classification. Leave out app_file_id to read bug reports across every build.
The first 100 bug reports are returned. Pass limit, up to 500, to get more in one response, and offset to page through the rest.
steps is a single string with one step per line. resultDetails, appFile, attachment and externalTicket are null when the bug has none.
One bug report
Read a single report by its id, such as the id of a bug listed on a mission request. The response is one bug report in the same shape as above.
curl https://api.qualgent.ai/v1/bug-reports/9d0e8400-e29b-41d4-a716-446655440000 \
-H "X-Api-Key: qg_your_api_key_here"What you get back
Results are limited to the organization your API key belongs to.
| Response | Meaning |
|---|---|
200 with bug reports | The bug reports that matched |
200 with an empty bugs array | The build has no bug reports, or it does not belong to your organization |
401 | The API key is missing or not valid |
403 | Your organization's plan does not include API access |
404 | For a single bug report: it does not exist, has been deleted, or has not been published to you yet |
422 | A parameter is not valid, such as an app_file_id or bug id that is not a UUID |
When a bug appears
QualGent reviews the bugs it finds before publishing them to you. A bug appears here at the moment it appears in bugs on the mission request, and not before. Until then, and after a bug is deleted, GET /v1/bug-reports/{id} returns 404.
Downloading evidence
attachment.downloadUrl is a pre-signed URL that is valid for one hour. Download the file when you read the report instead of storing the URL, and read the report again when you need a fresh one.
Organizing with categories
Categories are org-scoped groupings used to organize test cases by feature area, user flow, or release scope. They're created in the dashboard, not via API — but you'll use their IDs constantly when listing and running tests.
# 1. Fetch all categories
curl https://api.qualgent.ai/v1/categories/list \
-H "X-Api-Key: qg_your_api_key_here"
# 2. List only the test cases in a specific category
curl "https://api.qualgent.ai/v1/test-cases/list?category=cat_checkout" \
-H "X-Api-Key: qg_your_api_key_here"Common category patterns teams use:
- By feature —
Onboarding,Checkout,Profile. Run the full category when merging PRs that touch that area. - By severity —
Smoke,Regression,Exploratory. RunSmokeon every PR,Regressionnightly. - By release —
v2-launch,holiday-2026. Temporary buckets for focused QA sprints.
Using test case variables
Test cases can declare named variables and reference them inside step text by wrapping the variable's name in double curly braces — e.g. a variable named name is referenced as {{name}}. When you request a run, supply values through the vars field of POST /v1/missions/submit; it is accepted only when the request contains a single test case. This lets a single test case cover many input permutations (different branches, timeouts, search terms) without duplicating it.
Declaring variables on a test case
Variables are declared in the QualGent dashboard (or via the test-case CRUD endpoints) as an array on the test case itself:
{
"name": "Search and verify result",
"steps": [
{ "description": "Open the app and tap the search bar" },
{ "description": "Type {{query}} and submit" },
{ "description": "Wait up to {{timeout_seconds}} seconds for results" }
],
"variables": [
{ "name": "query", "type": "string", "required": true, "description": "Search term to type" },
{ "name": "timeout_seconds", "type": "number", "required": false, "default": 30 }
]
}| Field | Description |
|---|---|
name Required | The variable's key — what you wrap in {{ }} inside step text to reference this variable. Must match /^[a-z][a-z0-9_]*$/, up to 40 chars, and be unique within the test case. |
type Required | "string" or "number" |
required Optional | When true, callers must supply a value at run time unless a default is set. Defaults to false. |
default Optional | Value used when the caller omits this variable. Must match type. |
description Optional | Human-readable description (max 200 chars). Shown in tooltips and the run form. |
How resolution works
- Single-pass. Tokens in step text are substituted once. A resolved value containing
{{...}}is not re-resolved. - Declared-only. A
{{key}}reference is replaced only when a variable with that exact key is declared on the test case. Undeclared references pass through as literal text — useful when steps need to contain literal{{...}}syntax. - Defaults. Effective value = supplied value, falling back to the declared default.
CI/CD integration pattern
The canonical CI recipe: upload the build, request runs, poll the request until it completes, and fail the pipeline on any verified bug. A request can take hours, so run this in a job that is allowed to wait, or split submitting and collecting results into separate pipeline runs.
import os, time, sys, requests
API = "https://api.qualgent.ai/v1"
H = {"X-Api-Key": os.environ["QUALGENT_API_KEY"]}
# 1. Upload build
with open("build/app.apk", "rb") as f:
up = requests.post(f"{API}/apps/upload", headers=H,
files={"file": f},
data={"app_name": "MyApp", "version": os.environ["GITHUB_SHA"][:7]}).json()
app_id = up["file"]["id"]
# 2. Request runs for the smoke set
resp = requests.post(f"{API}/missions/submit", headers=H, json={
"test_case_ids": os.environ["SMOKE_TEST_IDS"].split(","),
"app_file_id": app_id,
})
if resp.status_code == 409:
sys.exit(f"A request is already open: {resp.json()['open_run']['id']}")
resp.raise_for_status()
result = resp.json()
if result["failed"] or not result["submission_id"]:
sys.exit(f"Not submitted: {result['failures']}")
submission_id = result["submission_id"]
# 3. Poll until every result has been published
while True:
time.sleep(300)
poll = requests.get(f"{API}/missions/submissions", headers=H,
params={"submission_id": submission_id})
if poll.status_code == 404:
sys.exit("The request was cancelled or replaced")
if poll.status_code >= 500:
continue # transient; try again on the next interval
poll.raise_for_status()
request = poll.json()["requests"][0]
print(request["stage"], request["progress"])
if request["stage"] == "complete":
break
# 4. Report anything not covered, then fail on a cancelled request or a verified bug
for item in request["notCovered"]:
print(f"Not covered: {item['name']} ({item['note']})")
if request["cancelled"]:
sys.exit("The request was cancelled")
for bug in request["bugs"]:
print(f"{bug['bugId']}: {bug['summary']}")
sys.exit(1 if request["bugs"] else 0)# GitHub Actions step
- name: QualGent smoke tests
env:
QUALGENT_API_KEY: ${{ secrets.QUALGENT_API_KEY }}
run: |
UP=$(curl -s https://api.qualgent.ai/v1/apps/upload \
-H "X-Api-Key: $QUALGENT_API_KEY" \
-F "file=@build/app.apk" -F "app_name=MyApp" -F "version=$GITHUB_SHA")
APP_ID=$(echo "$UP" | jq -r .file.id)
echo "app_id=$APP_ID" >> $GITHUB_OUTPUT
# ... submit & poll omitted for brevityPolling budget
The 10 req/s rate limit is intended for burst submission, not sustained polling. A request can take hours, so poll every few minutes rather than every few seconds.
Reusing builds across pipelines
If the same artifact feeds several requests, upload once and pass the app_file_id through step outputs.
Errors & retries
The API uses conventional HTTP codes, plus a stable error string you can branch on. Most transient errors are safe to retry with back-off; most permanent errors are not.
| Code | Meaning | Retry? |
|---|---|---|
401 | Missing/malformed/revoked API key (must start with qg_) | No — fix the key |
403 | Non-enterprise organization, or missing internal verification token | No — contact sales |
404 S-90004 | Resource doesn't exist in your org | No |
400 | Bad request — malformed body or invalid parameters | No |
409 run_in_progress | Your organization already has an open mission request | Only with replace_open, or once the open request completes |
413 | Upload exceeds MAX_SINGLE_UPLOAD_MB | No — split or strip the build |
429 | Rate limit exceeded | Yes — read Retry-After |
5xx | Upstream issue | Yes — exponential back-off |
Recommended back-off
Exponential with jitter, base 1s, cap 30s. Most HTTP clients ship a retry middleware that reads Retry-After — use it.
Idempotency
POST /v1/apps/delete is idempotent: deleting an already-soft-deleted file is a no-op and returns success. POST /v1/missions/submit is not idempotent: if it returns 504 the request may still have been created, so check GET /v1/missions/submissions before sending it again.
API reference
Complete reference for every REST endpoint in the QualGent API — parameters, response fields, and runnable code samples.
Apps
An App represents an uploaded mobile build (APK, IPA, or AAB). Upload once, reference many times.
Upload app
Upload a mobile application file, sent as multipart/form-data. AAB files are auto-converted to a universal APK.
Parameters
| file Required | The application file. Accepted: .apk, .ipa, .aab |
| app_name Required | Name of the application |
| version Required | Version string, e.g. 1.0.0 |
| os Optional | Platform. Auto-detected from file extension if omitted |
| package_name Optional | Android package name or iOS bundle ID |
List apps
Return all non-deleted app files in your organization, newest first.
Response fields
| id | Unique file identifier |
| name | Original filename |
| version | Application version |
| os | android or ios |
| link | Pre-signed download URL (valid 5 minutes) |
Get latest app
Return the most recently uploaded app in your org, with a pre-signed download link. Same response as Get app by ID.
Get app by ID
Return a single app with a pre-signed download link. To fetch the newest app without knowing its ID, use GET /v1/apps/latest.
Path parameters
| id Required | The app UUID |
Delete app
Soft-delete one or more app files by ID. Deleted files are hidden from list and get responses.
Parameters
| files Required | Array of app IDs to delete |
Test cases
A TestCase is a reusable scenario authored in the QualGent dashboard. The API lets you list them and inspect their metadata; to run them, see Missions. Test cases may declare a variables schema; each variable's key can be referenced inside step text as {{key}} — see Using test case variables.
List available tests
List all test cases in your organization, newest first. Optionally filter by category.
Query parameters
| category Optional | Category ID. Only cases in this category are returned. |
Missions
A Submission is one request to run test cases against a build. It goes through the same flow as the Run button in the QualGent dashboard: QualGent executes the tests, reviews the results, and reports verified bugs back to you. An organization has one open request at a time.
Request runs
Request runs for up to 200 test cases against one build. Returns 409 with code: "run_in_progress" while your organization already has an open request.
Parameters
| test_case_ids Required | Array of test case IDs to run |
| app_file_id Optional | Returned by POST /v1/apps/upload. Defaults to your organization's latest uploaded app. Returns 404 if you have no app, unless app_install_source is store_listing. |
| app_install_source Optional | uploaded_app (default) or store_listing |
| vars Optional | Object of values for the test case's declared variables. Only accepted when exactly one test case is requested. |
| replace_open Optional | Set to true to cancel and remove the open request and start this one |
Response fields
| submitted | Number of test cases submitted |
| failed | Number of test cases that could not be submitted |
| failures | Array of { test_case_id, error }, one per failed test case |
| submission_id | ID of the request, used to follow progress |
List requests
Return your organization's requests and their progress, most recent first.
Query parameters
| submission_id Optional | Return only this request |
| before Optional | ISO 8601 time; return the page of requests made before it |
Response fields
| requests[].id | Request identifier |
| requests[].stage | received, testing, verifying, or complete |
| requests[].progress | overall, tested and verified as percentages 0–100 |
| requests[].minutesLeft | Estimated minutes left in the current stage, when known |
| requests[].testCases | The test cases requested |
| requests[].bugs | Verified bugs found |
| requests[].notCovered | Test cases that could not be covered, with a note |
| requests[].canCancel | Whether the request can still be cancelled |
Cancel a request
Cancel a whole request. Only possible while nothing in it has started; otherwise returns 409. A cancelled request is removed.
Path parameters
| submission_id Required | The ID of the request |
Bug reports
A BugReport is a bug found on one of your builds, with its steps, expected and actual results, severity, and evidence. A bug found by QualGent appears once QualGent has reviewed it.
List bug reports
Return your organization's bug reports, most severe first and newest first within a severity. Pass app_file_id to get the bug reports for one build. The request has no body; every filter is a query parameter.
Query parameters
| app_file_id Optional | Return only bug reports for this build. The build's id, as returned by POST /v1/apps/upload or GET /v1/apps/list |
| status Optional | open, in_progress, fixed, or wont_fix |
| severity Optional | low, medium, high, or critical |
| failure_classification Optional | app_bug or setup_issue |
| limit Optional | Maximum number of bug reports to return, 1–500. Defaults to 100 |
| offset Optional | Number of bug reports to skip. Defaults to 0 |
Response fields
| bugs[].id | Bug identifier |
| bugs[].bugId | Human-readable ID within your organization, such as BUG_014 |
| bugs[].summary | One-line description of the bug |
| bugs[].steps | Steps to reproduce, one per line |
| bugs[].resultDetails | { expected, actual }, or null |
| bugs[].status | open, in_progress, fixed, or wont_fix |
| bugs[].severity | low, medium, high, or critical |
| bugs[].failureClassification | app_bug for a defect in the app, setup_issue for a missing prerequisite such as a test account, credential, or permission |
| bugs[].appFileId | ID of the build the bug was found on |
| bugs[].appFile | { id, appName, version, os, originalName } of that build |
| bugs[].attachment | { name, type, size, downloadUrl } of the screenshot or recording, or null. downloadUrl is valid for one hour |
| bugs[].externalTicket | { provider, key, url } of the linked GitHub, Jira, or Linear ticket, or null |
| bugs[].createdAt | When the bug was filed |
| bugs[].updatedAt | When the bug was last changed |
Get a bug report
Return one bug report, in the same shape as an entry of bugs above. Returns 404 when the bug does not exist, has been deleted, or has not been published to you yet.
Path parameters
| bug_report_id Required | The id of the bug, as returned by GET /v1/bug-reports or in bugs on GET /v1/missions/submissions |
Categories
A Category groups related test cases (e.g. "Smoke", "Regression", "Payments"). Use category IDs to filter listings and batch runs.
List categories
Return every category defined in your organization.
Response fields
| id | Category UUID |
| name | Human-readable category name |
| test_case_count | Number of test cases in this category |
Resources
Reference material — HTTP status codes and error response shapes.
Error codes
QualGent uses conventional HTTP response codes to indicate the success or failure of an API request. Codes in the 2xx range indicate success. Codes in the 4xx range indicate an error caused by the request. Codes in the 5xx range indicate an error with QualGent's servers.
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request — missing a required parameter |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — key lacks permission for this request |
| 404 | Not Found — the resource does not exist |
| 409 | Conflict — the request conflicts with current state, such as an open mission request |
| 413 | Payload Too Large — request entity exceeds limits |
| 422 | Unprocessable Entity — valid shape, invalid data |
| 429 | Too Many Requests — rate limit exceeded |
| 500 | Internal Server Error — something went wrong on QualGent's end |
| 502 | Bad Gateway — an upstream QualGent service returned an error |
| 504 | Gateway Timeout — an upstream QualGent service did not answer in time |
HTTP status reference
Example error response body:
{
"error": "A run is already in progress",
"code": "run_in_progress",
"open_run": {
"id": "7b0e8400-e29b-41d4-a716-446655440000",
"submitted_at": "2026-10-01T15:00:00.000Z",
"build": "MyApp 1.4.2"
}
}Changelog
Breaking changes are announced at least 30 days before rollout. Non-breaking additions ship continuously. Every entry links to the guide section with deeper context.
2026
GET /v1/apps/latest
Returns your organization's most recently uploaded app with a 5-minute pre-signed download link, in the same shape as GET /v1/apps/{id}. Passing latest as the ID still works. See Get latest app.
app_file_id is optional on mission submit; lists return newest first
POST /v1/missions/submit no longer requires app_file_id. When it is omitted, your organization's latest uploaded app is used, and the request returns 404 if you have no app unless app_install_source is store_listing. GET /v1/apps/list and GET /v1/test-cases/list now return the newest records first. Download links for app files with spaces in their names are now valid URLs. See Request runs.
latest_completed_run on GET /v1/test-cases/list
Test case list items no longer carry run data. Each item has id, name, status and category. Follow runs through GET /v1/missions/submissions instead. See List available tests.
Bug reports: /v1/bug-reports
GET /v1/bug-reports returns your organization's bug reports with their steps, expected and actual results, severity, and evidence. Pass app_file_id to get the reports for one build, or read a single report with GET /v1/bug-reports/{bug_report_id}. See Reading bug reports.
Missions: request runs through /v1/missions
POST /v1/missions/submit requests runs for a set of test cases against a build, using the same flow as the Run button in the dashboard. Follow a request with GET /v1/missions/submissions and cancel one with POST /v1/missions/submissions/{submission_id}/cancel. See Requesting runs with missions.
GET /v1/devices
The device listing endpoint has been removed.
Test case variables & vars on jobs
Test cases can now declare named variables; each variable's name is referenced inside step text by wrapping it in double curly braces — e.g. a variable named name is referenced as {{name}}. Pass jobs[].vars on POST /v1/test-cases/run to substitute values at run time; resolved steps are persisted on the test run. See Using test case variables.
use_sim flag for SMS OTP flows
Jobs can now declare use_sim: true to route execution to a SIM-equipped physical device capable of receiving real inbound SMS.
Category filter on run-all
POST /v1/test-cases/run-all now accepts an optional category_id. Only test cases in that category are executed.
Test case version pinning
Jobs accept test_case_version_id to pin execution to a specific historical version. The run record persists both version_id and version_number.
2025
Chunked uploads raised to 8 MB
Server-side chunking for builds at or above 32 MB now uses 8 MB segments (was 4 MB). Callers still see a single multipart request — no client-side changes required.
GET /v1/apps/latest shorthand
Returns the most recently uploaded build for your organization. Omitting app_file_id on a run submission resolves to this build, and the explicit endpoint is convenient for debugging.
Rate limit headers on every response
X-RateLimit-Remaining and X-RateLimit-Reset are now returned on every /v1 response, not only 429s. Use them to pace long-running pollers proactively.
Transactional batch submission
POST /v1/test-cases/run now creates all test_runs + queue entries atomically. A failure mid-batch rolls back every row, so you never end up with phantom jobs.
Public /v1 API launch
Initial release of the public QualGent API: Apps, Test cases, Jobs, Categories, Devices. Enterprise-only at launch; see Authentication.