QualGent Docs
⌘K
About the Platform Sign in Create account

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.

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

    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:

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

    Request runs

    Submit a set of test cases against your upload via the Missions API.

    3

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

    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, 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)
    }
    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

    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"])
    Response
    [
      {
        "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"
      }'
    Response (201)
    {
      "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"])
    Response
    {
      "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:

    StageMeaning
    receivedThe request is recorded and nothing has started yet
    testingAt least one test case has started
    verifyingEvery test case has a result and QualGent is reviewing them
    completeEvery 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"])
    Response
    {
      "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.

    ResponseMeaning
    200 with bug reportsThe bug reports that matched
    200 with an empty bugs arrayThe build has no bug reports, or it does not belong to your organization
    401The API key is missing or not valid
    403Your organization's plan does not include API access
    404For a single bug report: it does not exist, has been deleted, or has not been published to you yet
    422A 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. Run Smoke on every PR, Regression nightly.
    • 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:

    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.

    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 brevity

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

    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 — malformed body or invalid parametersNo
    409 run_in_progressYour organization already has an open mission requestOnly with replace_open, or once the open request completes
    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. 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.

    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, newest first.

    Response fields

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

    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 /v1/apps/{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 RequiredThe app UUID
    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 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.

    GET /v1/test-cases/list

    List available tests

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

    Query parameters

    category OptionalCategory 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.

    POST /v1/missions/submit

    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 RequiredArray of test case IDs to run
    app_file_id OptionalReturned 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 Optionaluploaded_app (default) or store_listing
    vars OptionalObject of values for the test case's declared variables. Only accepted when exactly one test case is requested.
    replace_open OptionalSet to true to cancel and remove the open request and start this one

    Response fields

    submittedNumber of test cases submitted
    failedNumber of test cases that could not be submitted
    failuresArray of { test_case_id, error }, one per failed test case
    submission_idID of the request, used to follow progress
    GET /v1/missions/submissions

    List requests

    Return your organization's requests and their progress, most recent first.

    Query parameters

    submission_id OptionalReturn only this request
    before OptionalISO 8601 time; return the page of requests made before it

    Response fields

    requests[].idRequest identifier
    requests[].stagereceived, testing, verifying, or complete
    requests[].progressoverall, tested and verified as percentages 0–100
    requests[].minutesLeftEstimated minutes left in the current stage, when known
    requests[].testCasesThe test cases requested
    requests[].bugsVerified bugs found
    requests[].notCoveredTest cases that could not be covered, with a note
    requests[].canCancelWhether the request can still be cancelled
    POST /v1/missions/submissions/{submission_id}/cancel

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

    GET /v1/bug-reports

    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 OptionalReturn only bug reports for this build. The build's id, as returned by POST /v1/apps/upload or GET /v1/apps/list
    status Optionalopen, in_progress, fixed, or wont_fix
    severity Optionallow, medium, high, or critical
    failure_classification Optionalapp_bug or setup_issue
    limit OptionalMaximum number of bug reports to return, 1–500. Defaults to 100
    offset OptionalNumber of bug reports to skip. Defaults to 0

    Response fields

    bugs[].idBug identifier
    bugs[].bugIdHuman-readable ID within your organization, such as BUG_014
    bugs[].summaryOne-line description of the bug
    bugs[].stepsSteps to reproduce, one per line
    bugs[].resultDetails{ expected, actual }, or null
    bugs[].statusopen, in_progress, fixed, or wont_fix
    bugs[].severitylow, medium, high, or critical
    bugs[].failureClassificationapp_bug for a defect in the app, setup_issue for a missing prerequisite such as a test account, credential, or permission
    bugs[].appFileIdID 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[].createdAtWhen the bug was filed
    bugs[].updatedAtWhen the bug was last changed
    GET /v1/bug-reports/{bug_report_id}

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

    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

    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
    409Conflict — the request conflicts with current state, such as an open mission request
    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
    502Bad Gateway — an upstream QualGent service returned an error
    504Gateway Timeout — an upstream QualGent service did not answer in time

    HTTP status reference

    Example error response body:

    409 Conflict
    {
      "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

    Oct 6, 2026
    Added

    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.

    Oct 6, 2026
    Changed

    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.

    Oct 6, 2026
    Removed

    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.

    Oct 5, 2026
    Added

    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.

    Oct 2, 2026
    Added

    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.

    Oct 2, 2026
    Removed

    GET /v1/devices

    The device listing endpoint has been removed.

    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.

    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.

    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 resolves to this build, and 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.