curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "pr-123"]
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"instructions": "Navigate to the main menu and verify all buttons are clickable",
"name": "PR #123 Smoke Test",
"additional_tags": ["pr-123", "smoke"]
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"player_deployment_configs": {
"2": { "deployment_config_id": "9a1b2c3d-4e5f-6789-a0b1-c2d3e4f5a6b7" }
}
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"run_on_all_configured_devices": true
}'
const response = await fetch("https://nunu.ai/api/v1/project/your-project-id/runs", {
method: "POST",
headers: {
"X-Api-Key": process.env.NUNU_API_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
multiplayer_test_id: "789e0123-e89b-12d3-a456-426614174000",
deployment_config_id: "456e7890-e89b-12d3-a456-426614174000",
build_id: "123e4567-e89b-12d3-a456-426614174000", // optional: use specific build
additional_tags: ["ci", "pr-123"],
}),
});
const data = await response.json();
import requests
import os
response = requests.post(
"https://nunu.ai/api/v1/project/your-project-id/runs",
headers={
"X-Api-Key": os.environ["NUNU_API_TOKEN"],
"Content-Type": "application/json"
},
json={
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "pr-123"]
}
)
data = response.json()
{
"message": "successfully queued test!",
"build_id": "f14fa698-ee3e-4a3d-935d-bdbeb874ad25",
"job_ids": [
"cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
]
}
{
"test_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"job_ids": [
"cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
]
}
{
"error": "multiplayer test not found"
}
Start a Run
Start a new test run using a multiplayer test, a test plan, or ephemeral instructions.
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "pr-123"]
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"instructions": "Navigate to the main menu and verify all buttons are clickable",
"name": "PR #123 Smoke Test",
"additional_tags": ["pr-123", "smoke"]
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"player_deployment_configs": {
"2": { "deployment_config_id": "9a1b2c3d-4e5f-6789-a0b1-c2d3e4f5a6b7" }
}
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"run_on_all_configured_devices": true
}'
const response = await fetch("https://nunu.ai/api/v1/project/your-project-id/runs", {
method: "POST",
headers: {
"X-Api-Key": process.env.NUNU_API_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
multiplayer_test_id: "789e0123-e89b-12d3-a456-426614174000",
deployment_config_id: "456e7890-e89b-12d3-a456-426614174000",
build_id: "123e4567-e89b-12d3-a456-426614174000", // optional: use specific build
additional_tags: ["ci", "pr-123"],
}),
});
const data = await response.json();
import requests
import os
response = requests.post(
"https://nunu.ai/api/v1/project/your-project-id/runs",
headers={
"X-Api-Key": os.environ["NUNU_API_TOKEN"],
"Content-Type": "application/json"
},
json={
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "pr-123"]
}
)
data = response.json()
{
"message": "successfully queued test!",
"build_id": "f14fa698-ee3e-4a3d-935d-bdbeb874ad25",
"job_ids": [
"cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
]
}
{
"test_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"job_ids": [
"cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
]
}
{
"error": "multiplayer test not found"
}
project:operate-runsRequest Body
multiplayer_test_id, test_plan_id, or instructions:multiplayer_test_id: run an existing testtest_plan_id: run an existing test planinstructions: run an ephemeral (one-off) test without creating a test case first
test_plan_id and instructions.multiplayer_test_id
and instructions. This endpoint starts test plans with a single deployment
config; to supply per-slot configs for a cross-platform plan, use
Start Test Plan Run instead.multiplayer_test_id and test_plan_id.player_deployment_configs entry. Required, except for multiplayer tests
where player_deployment_configs covers every player.instructions. Defaults to "[api] ephemeral test".deployment_config_id. See Using Build Storage Builds below."1", "2", and so on). Only applies with multiplayer_test_id,
and keys must be within the test’s player count. The map does not need to
cover every player: any player without an entry falls back to the default
deployment_config_id. Omitting deployment_config_id is only allowed when
every player has an entry; a partial map without a default returns a 400
error listing the missing players. Private device configs are allowed, but
every player on a private device needs its own device; if the configs cannot
give each player a distinct device, the request returns a 400 error.instructions (ephemeral), "api" is automatically prepended to the provided tags.ERROR, TIMEOUT, STOPPED). Genuine test verdicts (FAILED, BLOCKED, SKIPPED) are final and are not retried. Only applies when using multiplayer_test_id. Must be between 0 and 10.multiplayer_test_id. Must be between 1 and 10.deployment_config_id. Supported for mobile deployment configurations
with multi-device runs enabled. Requires deployment_config_id and cannot
be combined with player_deployment_configs. For existing tests,
instances applies independently on each device, so the aggregate run
count is instances multiplied by the number of configured devices. Normal
concurrency and billing rules apply.min_seconds / max_seconds on the test body). Edit the test, or create a new version, when you want to change the budget. See Create a Test for the per-framework defaults and rules.Show options properties
Show options properties
"United States", "London", "San Francisco, California (US)"). Routes device traffic through a proxy in the specified region."auto" to automatically use coordinates matching the selected mobile_geolocation, or provide explicit coordinates: { "lat": "37.7749", "long": "-122.4194" }. Note: "auto" requires mobile_geolocation to be set.Response
Single-device requests return201 Created. Multi-device requests return
aggregate per-device results; see the fields below. The body contains the
started-run data directly (no { ok, status, message, data } envelope).
instructions) runs.started_count in multi-device existing-test and test-plan
responses.device_label, ok, status, and failure message; successful
items may include the upstream response under data.instructions) requests. Each item
includes device_label, ok, and, when available, status and message.201 when every device run starts and 207
when only some start. A request that cannot start any saved-test or test-plan
run returns 500.{ "error": "<message>" } with an appropriate HTTP status
(400, 401, 403, 404, 500).
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "pr-123"]
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"instructions": "Navigate to the main menu and verify all buttons are clickable",
"name": "PR #123 Smoke Test",
"additional_tags": ["pr-123", "smoke"]
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"player_deployment_configs": {
"2": { "deployment_config_id": "9a1b2c3d-4e5f-6789-a0b1-c2d3e4f5a6b7" }
}
}'
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"run_on_all_configured_devices": true
}'
const response = await fetch("https://nunu.ai/api/v1/project/your-project-id/runs", {
method: "POST",
headers: {
"X-Api-Key": process.env.NUNU_API_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
multiplayer_test_id: "789e0123-e89b-12d3-a456-426614174000",
deployment_config_id: "456e7890-e89b-12d3-a456-426614174000",
build_id: "123e4567-e89b-12d3-a456-426614174000", // optional: use specific build
additional_tags: ["ci", "pr-123"],
}),
});
const data = await response.json();
import requests
import os
response = requests.post(
"https://nunu.ai/api/v1/project/your-project-id/runs",
headers={
"X-Api-Key": os.environ["NUNU_API_TOKEN"],
"Content-Type": "application/json"
},
json={
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "pr-123"]
}
)
data = response.json()
{
"message": "successfully queued test!",
"build_id": "f14fa698-ee3e-4a3d-935d-bdbeb874ad25",
"job_ids": [
"cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
]
}
{
"test_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"job_ids": [
"cc6e3994-a714-4b6b-9879-0b3e53c0a3af"
]
}
{
"error": "multiplayer test not found"
}
Using Build Storage Builds
You can run tests against a specific build from Build Storage by providing thebuild_id parameter.
build_id parameter only works with deployment configs that use Build Storage as their build source. If the deployment config uses a different build source (e.g., direct URL), the request will return a 400 error.Example: Upload and Test
# 1. Upload a new build (see Build Storage docs)
BUILD_ID="123e4567-e89b-12d3-a456-426614174000"
# 2. Start a test using that build
curl -X POST "https://nunu.ai/api/v1/project/your-project-id/runs" \
-H "X-Api-Key: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"multiplayer_test_id": "789e0123-e89b-12d3-a456-426614174000",
"deployment_config_id": "456e7890-e89b-12d3-a456-426614174000",
"build_id": "123e4567-e89b-12d3-a456-426614174000",
"additional_tags": ["ci", "build-test"]
}'
Complete CI/CD Example
Here’s a complete example that starts a test and polls for completion:const API_TOKEN = process.env.NUNU_API_TOKEN;
const PROJECT_ID = process.env.NUNU_PROJECT_ID;
const BASE_URL = `https://nunu.ai/api/v1/project/${PROJECT_ID}`;
async function startAndWaitForTest(
testId: string,
deploymentConfigId: string
): Promise<any> {
// Start the test
const startResponse = await fetch(`${BASE_URL}/runs`, {
method: "POST",
headers: {
"X-Api-Key": API_TOKEN,
"Content-Type": "application/json",
},
body: JSON.stringify({
multiplayer_test_id: testId,
deployment_config_id: deploymentConfigId,
}),
});
const startResult = await startResponse.json();
if (!startResponse.ok) {
throw new Error(`Failed to start test: ${startResult.error}`);
}
const jobId = startResult.job_ids[0];
console.log(`Test started with job: ${jobId}`);
// Poll job for completion
while (true) {
const jobResponse = await fetch(`${BASE_URL}/jobs/${jobId}`, {
headers: { "X-Api-Key": API_TOKEN },
});
const job = await jobResponse.json();
if (job.status === "COMPLETED") {
const result = job.runs[0]?.result ?? "unknown";
console.log(`Test completed with result: ${result}`);
return job;
}
console.log(`Test still running... (status: ${job.status})`);
await new Promise((resolve) => setTimeout(resolve, 30000));
}
}
import requests
import os
import time
API_TOKEN = os.environ["NUNU_API_TOKEN"]
PROJECT_ID = os.environ["NUNU_PROJECT_ID"]
BASE_URL = f"https://nunu.ai/api/v1/project/{PROJECT_ID}"
def start_and_wait_for_test(test_id: str, deployment_config_id: str) -> dict:
headers = {"X-Api-Key": API_TOKEN}
# Start the test
start_response = requests.post(
f"{BASE_URL}/runs",
headers={**headers, "Content-Type": "application/json"},
json={
"multiplayer_test_id": test_id,
"deployment_config_id": deployment_config_id
}
)
start_result = start_response.json()
if not start_response.ok:
raise Exception(f"Failed to start test: {start_result.get('error')}")
job_id = start_result["job_ids"][0]
print(f"Test started with job: {job_id}")
# Poll job for completion
while True:
job_response = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=headers)
job = job_response.json()
if job["status"] == "COMPLETED":
result = job["runs"][0]["result"] if job.get("runs") else "unknown"
print(f"Test completed with result: {result}")
return job
print(f"Test still running... (status: {job['status']})")
time.sleep(30)
#!/bin/bash
set -e
API_TOKEN="${NUNU_API_TOKEN}"
TEST_ID="${NUNU_TEST_ID}"
DEPLOYMENT_ID="${NUNU_DEPLOYMENT_ID}"
PROJECT_ID="${NUNU_PROJECT_ID}"
BASE_URL="https://nunu.ai/api/v1/project/${PROJECT_ID}"
# Start the test
START_RESPONSE=$(curl -s -X POST "${BASE_URL}/runs" \
-H "X-Api-Key: ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"multiplayer_test_id\": \"${TEST_ID}\",
\"deployment_config_id\": \"${DEPLOYMENT_ID}\"
}")
JOB_ID=$(echo "$START_RESPONSE" | jq -r '.job_ids[0]')
if [ "$JOB_ID" == "null" ]; then
echo "Failed to start test: $START_RESPONSE"
exit 1
fi
echo "Test started with job: $JOB_ID"
# Poll job for completion
while true; do
JOB=$(curl -s "${BASE_URL}/jobs/${JOB_ID}" \
-H "X-Api-Key: ${API_TOKEN}")
STATUS=$(echo "$JOB" | jq -r '.status')
if [ "$STATUS" == "COMPLETED" ]; then
RESULT=$(echo "$JOB" | jq -r '.runs[0].result')
echo "Test completed with result: $RESULT"
[ "$RESULT" != "SUCCESS" ] && exit 1
exit 0
fi
echo "Test running... (status: $STATUS)"
sleep 30
done
Authorizations
API key for authentication
Path Parameters
The project ID. You can copy it from the project settings page or the project URL in the dashboard.
Body
- Option 1
- Option 2
UUID of the deployment configuration, used by every player that has no player_deployment_configs entry. Required, except for multiplayer tests where player_deployment_configs covers every player.
UUID of the test to run (provide this OR test_plan_id OR instructions)
UUID of the test plan to run (provide this OR multiplayer_test_id OR instructions). This endpoint starts test plans with a single deployment config; to supply per-slot configs for a cross-platform plan, use POST /test-plans/{testPlanId}/runs instead.
Test instructions to execute as an ephemeral (one-off) test. Mutually exclusive with multiplayer_test_id and test_plan_id.
UUID of a build from Build Storage to use for this run. Only works with build-storage deployment configs, and requires deployment_config_id.
Per-player deployment configs for cross-platform multiplayer tests, keyed by player number ("1", "2", and so on). Only applies with multiplayer_test_id. The map does not need to cover every player: any player without an entry falls back to the default deployment_config_id. Omitting deployment_config_id is only allowed when every player has an entry; a partial map without a default returns a 400 error. Private device configs are allowed, but every player on a private device needs its own device; if the configs cannot give each player a distinct device, the request returns a 400 error.
Show child attributes
Show child attributes
Display name for the run. Only applies when using instructions. Defaults to '[api] ephemeral test'.
UUID of an agent configuration to use
Tags to add to the run. When using instructions (ephemeral), the tag 'api' is automatically prepended.
Number of times to retry the run after an execution failure (ERROR, TIMEOUT, STOPPED). Genuine test verdicts (FAILED, BLOCKED, SKIPPED) are final and are not retried. Only applies when using multiplayer_test_id.
0 <= x <= 10Number of parallel instances to run. Only applies when using multiplayer_test_id.
1 <= x <= 10Start one independent run on every device configured in deployment_config_id. Supported for mobile deployment configurations with multi-device runs enabled. Requires deployment_config_id; for multiplayer tests, cannot be combined with player_deployment_configs. For multiplayer tests, instances applies independently on each device.
Enable template rendering
Key-value pairs for template variables
Per-run override knobs. The time budget is intentionally not overridable here; it lives on the test version itself (see min_seconds / max_seconds on the test body), so callers should edit the test (or create a new version) when they want to change the budget.
Show child attributes
Show child attributes
Response
Run started successfully
Response body for POST /runs and POST /test-plans/{testPlanId}/runs. Single-device responses are returned with HTTP 201. Multi-device responses include per-device results and aggregate counts; HTTP 201 means all devices started, HTTP 207 means partial success, and HTTP 500 means none started.
Whether at least one device run started. Returned for multi-device requests.
Array of job IDs created. Single test returns 1 job; test plan returns one job per test.
Build ID used for the run. Returned for multiplayer test and test plan runs.
UUID of the created ephemeral test. Only returned for ephemeral (instructions) runs.
Test plan execution ID. Returned for test plan runs.
For cross-platform test plan runs: the build pinned for each extra slot, keyed by slot index. Only present when slot configs resolved to builds.
Show child attributes
Show child attributes
Optional status message returned for multiplayer test and test plan runs.
Number of runs requested across configured devices. For multiplayer tests, includes the instances multiplier.
Number of runs started successfully. For multiplayer tests, includes the instances multiplier.
Alias for started_count in multi-device existing-test and test-plan responses.
Number of requested runs that failed to start.
Per-device start results for existing-test and test-plan requests.
Show child attributes
Show child attributes
Per-device start results for ephemeral (instructions) requests.
Show child attributes
Show child attributes