Mobile Apps
Use the mobile apps API to upload project binaries for mobile runs and webhook-triggered batches.
Uploading is a two-step process: first request a presigned upload URL, then upload the file directly to storage, and finally confirm the upload.
POST /projects/{projectId}/mobile/upload
| Field | Type | Required | Description |
|---|
filename | string | Yes | .app.zip, .app.tar.gz, .zip, .tgz, or .apk |
fileSize | integer (bytes) | Yes | Size of the file in bytes |
removeAfter | integer seconds | No | Auto-delete the upload after this many seconds |
{
"uploadUrl": "https://storage.example.com/presigned-put-url...",
"storageKey": "files/{projectId}/1234-abcd.zip",
"filename": "MyApp.app.zip",
"fileSize": 123456789,
"removeAfter": 3600
}
Upload the binary directly to the presigned URL returned in step 1. The presigned URL is valid for 30 minutes.
curl -X PUT "<uploadUrl>" \
-H "Content-Type: application/octet-stream" \
--data-binary @MyApp.app.zip
POST /projects/{projectId}/mobile/upload/confirm
Send a JSON body with the values from step 1:
| Field | Type | Required | Description |
|---|
storageKey | string | Yes | The storageKey returned in step 1 |
filename | string | Yes | The filename returned in step 1 |
fileSize | integer (bytes) | Yes | The fileSize returned in step 1 |
removeAfter | integer seconds | No | Auto-delete the upload after this many seconds |
{
"app": {
"id": "<YOUR APP UPLOAD ID>",
"platform": "ios",
"filename": "MyApp",
"bundleId": "com.example.app",
"appVersion": "1.2.3",
"buildVersion": "45",
"fileSize": 123456789,
"source":
# Step 1: Initiate
RESPONSE=$(curl -s -X POST https://tester.army/api/v1/projects/{projectId}/mobile/upload \
-H "Authorization: Bearer <YOUR API KEY>" \
-H "Content-Type: application/json" \
-d "{
\"filename\": \"MyApp.app.zip\",
\"fileSize\": $(stat -f%z MyApp.app.zip),
\"removeAfter
If removeAfter is omitted, the upload is permanent.
If the expiration time is reached while queued or running tests are still using the app, cleanup waits until those runs finish.
GET /projects/{projectId}/mobile
curl https://tester.army/api/v1/projects/{projectId}/mobile \
-H "Authorization: Bearer <YOUR API KEY>"
{
"apps": [
{
"id": "<YOUR APP UPLOAD ID>",
"platform": "ios",
"filename": "MyApp",
"bundleId": "com.example.app",
"appVersion": "1.2.3",
"buildVersion": "45",
"fileSize": 123456789,
"source"
DELETE /projects/{projectId}/mobile/{appId}
curl -X DELETE https://tester.army/api/v1/projects/{projectId}/mobile/{appId} \
-H "Authorization: Bearer <YOUR API KEY>"
After uploading, trigger a mobile group webhook with the uploaded app's appId or bundleId.
By app ID (recommended when multiple builds share the same bundle ID):
curl -X POST https://tester.army/api/v1/groups/webhook/{id}/{secret} \
-H "Content-Type: application/json" \
-d '{
"mobile": {
"appId": "<YOUR APP UPLOAD ID>"
}
}'
By bundle ID (resolves to the latest upload matching that identifier):
curl -X POST https://tester.army/api/v1/groups/webhook/{id}/{secret} \
-H "Content-Type: application/json" \
-d '{
"mobile": {
"bundleId": "com.example.app"
}
}'
Only one of appId, bundleId, or artifactUrl can be provided per request. See Group Webhooks for full details.
"api_upload"
,
"expiresAt": "2026-04-05T12:34:56.000Z",
"removeAfter": 3600,
"createdAt": "2026-04-05T11:34:56.000Z"
}
}
\"
: 3600
}")
UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.uploadUrl')
STORAGE_KEY=$(echo "$RESPONSE" | jq -r '.storageKey')
# Step 2: Upload directly to storage
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary @MyApp.app.zip
# Step 3: Confirm
curl -X POST https://tester.army/api/v1/projects/{projectId}/mobile/upload/confirm \
-H "Authorization: Bearer <YOUR API KEY>" \
-H "Content-Type: application/json" \
-d "{
\"storageKey\": \"$STORAGE_KEY\",
\"filename\": \"MyApp.app.zip\",
\"fileSize\": $(stat -f%z MyApp.app.zip),
\"removeAfter\": 3600
}"
:
"api_upload"
,
"expiresAt": null,
"removeAfter": null,
"createdAt": "2026-04-05T11:34:56.000Z"
}
]
}