QualGent Docs
⌘K
About the Platform Sign in Create account

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.

What's new View all →

    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

    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:

    1. Receive — the binary is read off the wire as a stream.
    2. 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.
    3. 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-After on 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:

    1

    Upload your application

    Submit your mobile build via the Apps API. The endpoint handles large files transparently — no client-side chunking required.

    2

    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.

    3

    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))
    }
    Response
    [
      {
        "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

    1. Create an account at app.qualgent.ai/auth/sign-up.
    2. From the dashboard, open Settings → Developer to manage API keys.
    3. Generate a new key, copy it somewhere safe, or revoke stale keys.

    Header format

    HeaderValueDescription
    X-Api-Keyqg_your_api_keyYour 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 },
    });
    Keep keys secret. Never publish keys in client-side code, public repos, or screenshots. Rotate any key that may have leaked.

    Rate limits

    Rate limits are applied per API key to ensure fair usage and system stability.

    LimitDescription
    10 req/secMaximum requests per second, per API key

    Every response includes usage headers:

    • X-RateLimit-Limit — maximum requests per second
    • X-RateLimit-Remaining — requests remaining in the current window
    • X-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)
    }
    Response
    {
      "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)
    Response
    {
      "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 featureOnboarding, Checkout, Profile. Run the full category when merging PRs that touch that area.
    • By severitySmoke, Regression, Exploratory. Run Smoke on every PR, Regression nightly.
    • By releasev2-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:

    Test case schema (excerpt)
    {
      "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 }
      ]
    }
    FieldDescription
    name RequiredThe 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 OptionalWhen true, callers must supply a value at run time unless a default is set. Defaults to false.
    default OptionalValue used when the caller omits this variable. Must match type.
    description OptionalHuman-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

    OutcomeWhen
    400 missing_required_variableA required variable was not supplied and has no default
    400 unresolved_referenceA step references a declared variable that has no effective value
    422 invalid_variable_valueA 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 brevity

    Polling 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.

    CodeMeaningRetry?
    401Missing/malformed/revoked API key (must start with qg_)No — fix the key
    403Non-enterprise organization, or missing internal verification tokenNo — contact sales
    404 S-90004Resource doesn't exist in your orgNo
    400Bad request — batch > 10 jobs, malformed body, etc.No
    402Insufficient credits for the batchOnly after topping up
    413Upload exceeds MAX_SINGLE_UPLOAD_MBNo — split or strip the build
    429Rate limit exceededYes — read Retry-After
    5xxUpstream issueYes — 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.

    POST /v1/apps/upload

    Upload app

    Upload a mobile application file, sent as multipart/form-data. AAB files are auto-converted to a universal APK.

    Parameters

    file RequiredThe application file. Accepted: .apk, .ipa, .aab
    app_name RequiredName of the application
    version RequiredVersion string, e.g. 1.0.0
    os OptionalPlatform. Auto-detected from file extension if omitted
    package_name OptionalAndroid package name or iOS bundle ID
    GET /v1/apps/list

    List apps

    Return all non-deleted app files in your organization.

    Response fields

    idUnique file identifier
    nameOriginal filename
    versionApplication version
    osandroid or ios
    linkPre-signed download URL (valid 5 minutes)
    GET /v1/apps/{id}

    Get app by ID or latest

    Pass latest to retrieve the most recently uploaded app in your org.

    Path parameters

    id RequiredThe app UUID, or the literal string latest
    POST /v1/apps/delete

    Delete app

    Soft-delete one or more app files by ID. Deleted files are hidden from list and get responses.

    Parameters

    files RequiredArray 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.

    GET /v1/test-cases/list

    List available tests

    List all test cases in your organization. Optionally filter by category.

    Query parameters

    category OptionalCategory ID. Only cases in this category are returned.
    POST /v1/test-cases/run

    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 RequiredArray of job configurations
    jobs[].app_file_id RequiredReturned by POST /v1/apps/upload
    jobs[].test_case_id RequiredTest case to run
    jobs[].device RequiredDevice configuration object
    jobs[].use_sim OptionalRequire a SIM-enabled device (for SMS OTP tests)
    jobs[].vars OptionalObject 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.
    POST /v1/test-cases/run-all

    Run all tests

    Run every active test case against the specified build. Use category_id to narrow scope.

    Parameters

    app_file_id RequiredThe app to test
    device RequiredDevice configuration
    category_id OptionalLimit 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.

    GET /v1/jobs/list

    List all jobs

    Return all test runs in your organization, most recent first.

    GET /v1/jobs/status/{test_run_id}

    Get job status

    Return the current status and details of a specific test run.

    Path parameters

    test_run_id RequiredThe ID of the test run

    Response fields

    idTest run identifier
    statusqueued, running, passed, or failed
    progressCompletion percentage 0–100
    prioritylow, medium, or high
    deviceDevice 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.

    GET /v1/categories/list

    List categories

    Return every category defined in your organization.

    Response fields

    idCategory UUID
    nameHuman-readable category name
    test_case_countNumber of test cases in this category

    Devices

    A Device specifies the hardware profile a test runs against. Query available devices before queuing a run.

    GET /v1/devices

    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
    *.nameDevice name, e.g. Pixel 7
    *.platformAndroid or iOS
    *.os_versionOS version string
    *.sms_enabledWhether the device can receive SMS OTPs
    Response
    {
      "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.

    CodeDescription
    200Success
    400Bad Request — missing a required parameter
    401Unauthorized — invalid or missing API key
    403Forbidden — key lacks permission for this request
    404Not Found — the resource does not exist
    413Payload Too Large — request entity exceeds limits
    422Unprocessable Entity — valid shape, invalid data
    429Too Many Requests — rate limit exceeded
    500Internal Server Error — something went wrong on QualGent's end

    HTTP status reference

    Example error response body:

    422 Unprocessable Entity
    {
      "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

    Apr 27, 2026
    Added

    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.

    Apr 12, 2026
    Added

    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.

    Jan 20, 2026
    Added

    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.

    Jan 08, 2026
    Added

    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

    Dec 11, 2025
    Changed

    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.

    Nov 04, 2025
    Added

    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.

    Sep 22, 2025
    Changed

    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.

    Jul 15, 2025
    Added

    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.

    May 06, 2025
    Added

    Public /v1 API launch

    Initial release of the public QualGent API: Apps, Test cases, Jobs, Categories, Devices. Enterprise-only at launch; see Authentication.