Skip to main content

Workflow Example

Use this example to upload an APK, trigger regression-tagged test runs, and wait for the overall execution to finish.

For shared connection details, see Overview.

Flow

  1. Upload the APK and capture the returned app_id.
  2. Trigger test flows with tags.include.type = ["regression"].
  3. Read the returned trigger_id.
  4. Poll the trigger status endpoint until the status is no longer in_process.

End-to-End Example

import json
import sys
import time

import requests


BASE_URL = "https://qa-app.getpanto.ai"
API_KEY = "your_api_key_here"
APK_PATH = "./app-release.apk"
APP_NAME = "Android Regression Build"

headers = {
"X-PANTO-API-KEY": API_KEY,
}


def upload_apk(apk_path: str, app_name: str) -> str:
with open(apk_path, "rb") as apk_file:
response = requests.post(
f"{BASE_URL}/api/v1/qa/external/app/upload",
headers=headers,
data={"app_name": app_name},
files={
"apk_file": (
"app-release.apk",
apk_file,
"application/vnd.android.package-archive",
)
},
timeout=120,
)

response.raise_for_status()
payload = response.json()
return payload["id"]


def trigger_test_flow(app_id: str) -> str:
response = requests.post(
f"{BASE_URL}/api/v1/qa/external/test-flow/trigger",
headers={**headers, "Content-Type": "application/json"},
json={
"app_id": app_id,
"tags": {
"include": {
"type": ["regression"]
}
},
"devices": [
{
"platform": "android",
"device_name": "pixel 8", // should be in lowercase
"platform_version": "14"
}
],
"env_name": "staging"
},
timeout=120,
)

response.raise_for_status()
payload = response.json()
return payload["trigger_id"]


def get_trigger_status(trigger_id: str) -> dict:
response = requests.get(
f"{BASE_URL}/api/v1/qa/external/test-runs-by-trigger/{trigger_id}",
headers=headers,
timeout=120,
)
response.raise_for_status()
return response.json()


def wait_for_completion(trigger_id: str, poll_interval_seconds: int = 40) -> dict:
while True:
payload = get_trigger_status(trigger_id)
status = payload["status"]

print(f"Current status: {status}")

if status != "in_process":
return payload

time.sleep(poll_interval_seconds)


def main() -> None:
app_id = upload_apk(APK_PATH, APP_NAME)
print(f"Uploaded app_id: {app_id}")

trigger_id = trigger_test_flow(app_id)
print(f"Triggered regression runs with trigger_id: {trigger_id}")

final_payload = wait_for_completion(trigger_id)
final_status = final_payload["status"]

print(f"Final result: {final_status}")
print(json.dumps(final_payload, indent=2))

if final_status == "passed":
sys.exit(0)

if final_status in {"failed", "partially_passed"}:
sys.exit(1)

sys.exit(2)


if __name__ == "__main__":
main()

Example Response Sequence

Upload response:

{
"id": "app_01",
"name": "Android Regression Build",
"created_at": "2026-05-21T10:30:00Z",
"updated_at": "2026-05-21T10:30:00Z"
}

Trigger response:

{
"trigger_id": "trigger_01",
"message": "Triggered 2 test flow(s) matched.",
"test_runs": [
"testrun_01",
"testrun_02"
]
}

Polling response while running:

{
"test_runs": [
{
"id": "testrun_01",
"runner_status": "running",
"trigger_id": "trigger_01"
},
{
"id": "testrun_02",
"runner_status": "queued",
"trigger_id": "trigger_01"
}
],
"status": "in_process"
}

Polling response after completion:

{
"test_runs": [
{
"id": "testrun_01",
"runner_status": "passed",
"trigger_id": "trigger_01"
},
{
"id": "testrun_02",
"runner_status": "passed",
"trigger_id": "trigger_01"
}
],
"status": "passed"
}

Notes

  • This example uses Python 3 and the requests package.
  • Install the dependency with pip install requests before running the script.
  • The script exits with code 0 for passed, 1 for failed or partially_passed, and 2 for any unexpected final status.
  • If you want to target test suites instead of test flows, replace the trigger endpoint with /api/v1/qa/external/testsuite/trigger and keep the same polling logic.
  • If the final status is failed or partially_passed, inspect the returned test_runs and fetch individual runs from the Test Runs endpoint for more detail.