Tour of the API
See how QualGent API objects (apps, test cases, jobs, devices) 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 test run 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, your library of reusable test scenarios are TestCase objects, and every queued, running, or completed execution is a Job. A Device describes the hardware and OS version a job runs against; a Category groups related test cases.
Objects have lives
Most objects progress through states. A Job starts as queued, moves to running, and ends as passed or failed. An App is uploaded once and then referenced by ID across many jobs. 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, kicks off a batch of jobs against that build, and polls status until every job settles. No single endpoint does all three — your integration is the choreography between Apps, Test cases, and Jobs.
Key features
Test execution
Run individual test cases or execute your entire suite against uploaded applications.
CI/CD integration
Plug automated testing directly into your GitHub Actions, CircleCI, or Jenkins pipeline.
Job monitoring
Track status, progress, and detailed results for every queued and completed test run.
Multi-platform support
Target both Android and iOS devices with flexible device configurations.
The path a test takes
Here's what actually happens between the moment you POST an APK and the moment a passed status lands back in your CI log. The QualGent platform is a handful of cooperating services — FastAPI, a Postgres queue, a pool of real devices, and an AI agent — and understanding how they hand work off to each other makes debugging, retries, and parallelism 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 batch of jobs
POST /v1/test-cases/run accepts up to 10 jobs per request. Before any job is queued, the API verifies your organization has enough credits to cover the entire batch. If not, the request is rejected with 402 Payment Required and nothing is queued — there is no partial success.
If the credit check passes, every job in the batch is queued atomically. If any job in the batch fails to enqueue, the entire batch is rolled back — you never end up with a phantom job that nobody is going to execute.
If you omit app_file_id, the API automatically uses your most recently uploaded build. Handy for nightly runs against the latest artifact.
3. Execute on a real device
Once dispatched, the job is handed to QualGent's AI agent on the target device (or simulator). The agent reads the test case's plain-English steps and drives the app directly — tapping, typing, scrolling, reading OTPs over SMS if sms_enabled is true. As it works, it updates status to "running" and emits a progress integer from 0 to 100.
Two execution modes are supported: agent (the default — full AI-driven execution) and cached run (deterministic replay for scenarios that have already been recorded).
4. Terminal state & results
When the agent finishes, the job transitions to a terminal state:
passed— agent completed successfully and assertions held.failed— agent completed but the test case failed, or a device/app crash occurred.completed— neutral terminal state for runs without a pass/fail signal.
Every terminal-state response carries progress = 100 and a link to the full result detail page at https://app.qualgent.ai/test-runs/{id}. The detail page has the full execution trace, step-by-step screenshots, and a plain-English failure explanation from the agent — useful for triage, not usually needed for gate-on-green CI.
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.
Run your tests
Queue runs against your upload via the Test cases API. Execute single tests, or a full suite with up to 10 in parallel per batch.
Get results
Poll the Jobs API in real time — status, progress, and comprehensive reports including pass/fail state and execution logs.
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" },
"latest_completed_run": {
"id": "run_abc123def456",
"status": "passed",
"progress": 100,
"device": "iPhone 14 Pro"
}
}
]If everything worked, you'll get a JSON array of your organization's test cases. From here, head to the Handling test cases guide to start queueing 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, run test cases, and monitor jobs.
Managing your applications
Upload the mobile application you want to test, then reference it by ID in every subsequent job. 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
Once a build is uploaded, queue runs in one of two flavors: specific cases by ID, or the entire suite filtered by category.
Run specific test cases
curl https://api.qualgent.ai/v1/test-cases/run \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"jobs": [{
"app_file_id": "3f9a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"test_case_id": "tc_xyz789",
"device": {
"name": "Pixel 7",
"platform": "Android",
"os_version": "14",
"orientation": "portrait"
}
}]
}'import requests
response = requests.post(
"https://api.qualgent.ai/v1/test-cases/run",
headers={
"X-Api-Key": "qg_your_api_key_here",
"Content-Type": "application/json",
},
json={
"jobs": [{
"app_file_id": "3f9a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"test_case_id": "tc_xyz789",
"device": {
"name": "Pixel 7",
"platform": "Android",
"os_version": "14",
"orientation": "portrait",
},
}]
},
)
print(response.json())const response = await fetch("https://api.qualgent.ai/v1/test-cases/run", {
method: "POST",
headers: {
"X-Api-Key": "qg_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
jobs: [{
app_file_id: "3f9a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
test_case_id: "tc_xyz789",
device: {
name: "Pixel 7",
platform: "Android",
os_version: "14",
orientation: "portrait",
},
}],
}),
});Run the full suite
Execute every active test case against a specific build. Pass category_id to narrow scope — useful for targeted regression runs on PRs.
curl https://api.qualgent.ai/v1/test-cases/run-all \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"app_file_id": "3f9a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"device": { "name": "Pixel 7", "platform": "Android", "os_version": "14" },
"category_id": "770e8400-e29b-41d4-a716-446655440000"
}'import requests
response = requests.post(
"https://api.qualgent.ai/v1/test-cases/run-all",
headers={
"X-Api-Key": "qg_your_api_key_here",
"Content-Type": "application/json",
},
json={
"app_file_id": "3f9a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"device": {
"name": "Pixel 7",
"platform": "Android",
"os_version": "14",
},
"category_id": "770e8400-e29b-41d4-a716-446655440000",
},
)Monitor jobs
Every queued run is a Job with a status lifecycle: queued → running → passed | failed. Poll the status endpoint in your CI step and fail the build on any failed terminal state.
List all jobs
curl https://api.qualgent.ai/v1/jobs/list \
-H "X-Api-Key: qg_your_api_key_here"import requests
response = requests.get(
"https://api.qualgent.ai/v1/jobs/list",
headers={"X-Api-Key": "qg_your_api_key_here"},
)
for run in response.json()["test_runs"]:
print(run["id"], run["status"], run["progress"])Check job status
curl https://api.qualgent.ai/v1/jobs/status/run_abc123 \
-H "X-Api-Key: qg_your_api_key_here"import requests, time
def wait_for(run_id):
while True:
r = requests.get(
f"https://api.qualgent.ai/v1/jobs/status/{run_id}",
headers={"X-Api-Key": "qg_your_api_key_here"},
).json()
if r["status"] in ("passed", "failed"):
return r
time.sleep(5){
"id": "run_abc123",
"status": "running",
"progress": 45,
"priority": "medium",
"device": "Pixel 7",
"test_case": { "id": "tc_1234567890", "name": "Login Flow Test" },
"app": { "id": "file_xyz789", "name": "MyApp v1.0.0" }
}Choosing devices
Every job needs a target device. Query the live pool first — it only returns devices with availability = true at the moment of the request. Devices go offline for maintenance, reservations, or health-check failures, so don't hardcode the list.
curl https://api.qualgent.ai/v1/devices \
-H "X-Api-Key: qg_your_api_key_here"import requests
pool = requests.get(
"https://api.qualgent.ai/v1/devices",
headers={"X-Api-Key": "qg_your_api_key_here"},
).json()
# Pick the first SMS-capable phone
phone = next(d for d in pool["phone"] if d["sms_enabled"])The response groups devices by form factor — phone and tablet. Use a device's name and platform verbatim when submitting a run; those strings are the canonical values the dispatcher matches against.
SMS / OTP flows
For test cases that receive a one-time code over SMS, filter the pool for sms_enabled: true and set use_sim: true on the job. Only SIM-equipped physical devices can receive real inbound SMS.
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.
Pinning test case versions
Test cases are versioned in the QualGent dashboard — every edit creates a new immutable version. By default, a job runs the current version. For reproducibility (bug repros, CI determinism, long-running experiments), pin the job to a specific historical version.
curl https://api.qualgent.ai/v1/test-cases/run \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"jobs": [{
"app_file_id": "3f9a1b2c-...",
"test_case_id": "tc_xyz789",
"test_case_version_id": "v_20260401_a3f9",
"device": { "name": "Pixel 7", "platform": "Android" }
}]
}'The run record persists both the version_id and the human-readable version_number, so you can always trace what steps were actually executed — even after the test case has been edited many times since.
Audit / regression feedback
For flaky-test triage or regression hunts, pass audit_source_run_id to inject a prior run's execution trace as context for the AI agent. The agent uses it to recognize patterns that differ from the baseline and explain them in the failure report.
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}}. At run time, supply values through the vars field on each job — the API substitutes them into the steps before dispatching to the agent. 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. |
Supplying values at run time
Pass a vars object on each job. Keys must match declared variable names; values must match declared types. Numbers are stringified into the step text.
curl https://api.qualgent.ai/v1/test-cases/run \
-H "X-Api-Key: qg_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"jobs": [{
"app_file_id": "3f9a1b2c-...",
"test_case_id": "tc_xyz789",
"device": { "name": "Pixel 7", "platform": "Android" },
"vars": {
"query": "wireless headphones",
"timeout_seconds": 60
}
}]
}'import requests
requests.post(
"https://api.qualgent.ai/v1/test-cases/run",
headers={"X-Api-Key": "qg_your_api_key_here"},
json={
"jobs": [{
"app_file_id": "3f9a1b2c-...",
"test_case_id": "tc_xyz789",
"device": {"name": "Pixel 7", "platform": "Android"},
"vars": {
"query": "wireless headphones",
"timeout_seconds": 60,
},
}]
},
)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. A required variable with no default and no supplied value returns
400. - Resolved steps are persisted. The substituted step list is stored on the run, so later inspection always shows the exact text the agent saw.
Warnings & errors
| Outcome | When |
|---|---|
400 missing_required_variable | A required variable was not supplied and has no default |
400 unresolved_reference | A step references a declared variable that has no effective value |
422 invalid_variable_value | A supplied value's type does not match the declared type |
200 + warnings: [unknown_variable] | Supplied a key that is not declared on the test case. The key is dropped silently and a warning is returned alongside the created jobs. |
CI/CD integration pattern
The canonical CI recipe: upload the build, submit a batch, poll every job to a terminal state, fail the pipeline on any failed.
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. Queue a smoke batch
runs = requests.post(f"{API}/test-cases/run", headers=H, json={
"jobs": [
{"app_file_id": app_id, "test_case_id": tc,
"device": {"name": "Pixel 7", "platform": "Android"}}
for tc in os.environ["SMOKE_TEST_IDS"].split(",")
]
}).json()
run_ids = [r["test_run_id"] for r in runs["results"]]
# 3. Poll to terminal state (15s interval to respect rate limits)
pending = set(run_ids)
while pending:
time.sleep(15)
for rid in list(pending):
s = requests.get(f"{API}/jobs/status/{rid}", headers=H).json()
if s["status"] in ("passed", "failed", "completed"):
pending.remove(rid)
print(f"{rid}: {s['status']} — {s['link']}")
if s["status"] == "failed":
sys.exit(1)# 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 15-second interval is the recommended floor. For large batches, poll in round-robin across jobs rather than tight-looping each one.
Reusing builds across pipelines
If the same artifact feeds multiple jobs (e.g., a matrix of device targets), upload once and pass the app_file_id through step outputs. You can also omit app_file_id entirely and the API will auto-select your most recent build — useful for nightly runs against whatever last shipped.
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 — batch > 10 jobs, malformed body, etc. | No |
402 | Insufficient credits for the batch | Only after topping up |
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. Submissions to /v1/test-cases/run are not idempotent — retrying after a network timeout may enqueue duplicate jobs. Check GET /v1/jobs/list before retrying a submission you're uncertain about.
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.
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 app by ID or latest
Pass latest to retrieve the most recently uploaded app in your org.
Path parameters
| id Required | The app UUID, or the literal string latest |
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, run them, and inspect their metadata. Test cases may declare a variables schema; each variable's key can be referenced inside step text as {{key}} and a value is supplied per-run via jobs[].vars — see Using test case variables.
List available tests
List all test cases in your organization. Optionally filter by category.
Query parameters
| category Optional | Category ID. Only cases in this category are returned. |
Run individual tests
Queue up to 10 test runs in a single batch. Each job pairs a test case with an app and a target device.
Parameters
| jobs Required | Array of job configurations |
| jobs[].app_file_id Required | Returned by POST /v1/apps/upload |
| jobs[].test_case_id Required | Test case to run |
| jobs[].device Required | Device configuration object |
| jobs[].use_sim Optional | Require a SIM-enabled device (for SMS OTP tests) |
| jobs[].vars Optional | Object of supplied values for the test case's declared variables. Keys must match variable names declared on the test case; values must match declared types. Unknown keys are ignored with a warning. See Using test case variables. |
Run all tests
Run every active test case against the specified build. Use category_id to narrow scope.
Parameters
| app_file_id Required | The app to test |
| device Required | Device configuration |
| category_id Optional | Limit to cases in this category |
Jobs
Every queued run is a Job. Poll for status, list recent runs, or fetch a single run's details.
List all jobs
Return all test runs in your organization, most recent first.
Get job status
Return the current status and details of a specific test run.
Path parameters
| test_run_id Required | The ID of the test run |
Response fields
| id | Test run identifier |
| status | queued, running, passed, or failed |
| progress | Completion percentage 0–100 |
| priority | low, medium, or high |
| device | Device the run executed on |
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 |
Devices
A Device specifies the hardware profile a test runs against. Query available devices before queuing a run.
List devices
Return the full catalog of available devices, grouped by form factor.
Response fields
| phone[] | Array of phone device specs |
| tablet[] | Array of tablet device specs |
| *.name | Device name, e.g. Pixel 7 |
| *.platform | Android or iOS |
| *.os_version | OS version string |
| *.sms_enabled | Whether the device can receive SMS OTPs |
{
"phone": [
{ "name": "Pixel 7", "platform": "Android", "os_version": "14", "sms_enabled": true },
{ "name": "iPhone 14 Pro", "platform": "iOS", "os_version": "16.0", "sms_enabled": false }
],
"tablet": [
{ "name": "iPad Pro", "platform": "iOS", "os_version": "16.0", "sms_enabled": false }
]
}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 |
| 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 |
HTTP status reference
Example error response body:
{
"error": "invalid_request",
"message": "device.platform must be 'Android' or 'iOS'",
"details": { "field": "jobs[0].device.platform" }
}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
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. See SMS / OTP flows in the Guides.
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. See Pinning test case versions.
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 already resolves to this, but 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.