Legacy API (v1 Compatibility)
If you are migrating from TestFairy, your existing CI/CD scripts and plugins will continue to work without changes. The legacy API endpoints are fully supported alongside the new API v3.
Authentication
All endpoints require authentication. You can authenticate using any of the following methods:
| Method | Example |
|---|---|
X-API-Key header | curl -H "X-API-Key: YOUR_KEY" ... |
api_key POST param | curl -F api_key=YOUR_KEY ... |
Response Format
All responses return JSON with a status field ("ok" or "fail").
Success
{ "status": "ok", ... }
Error
{ "status": "fail", "code": 5, "message": "..." }
Upload
POST /api/upload
Upload an APK, AAB, or IPA file. The app is automatically matched by package name, or created if it doesn't exist.
curl https://app.testfairy.com/api/upload \
-F api_key=YOUR_API_KEY \
-F file=@app-release.apk \
-F changelog="Bug fixes and improvements" \
-F notify=on \
-F groups="QA,Beta"
| Parameter | Required | Default | Description |
|---|---|---|---|
file | Yes | — | Binary file (.apk, .aab, or .ipa). Also accepted as apk_file |
team_id | No | Auto-detected | Team to upload to. Required when you belong to several teams and the app doesn't exist yet, or when it exists on more than one of your teams (error code 136) |
folder_name | No | None | Assign the app to a folder |
app_version | No | Detected from the file | Override the version string. Also accepted as version |
version_code | No | Detected from the file | Override the version code |
changelog | No | None | Release notes. Also accepted as comment or release_notes |
tags | No | None | Comma-separated tags for the build |
groups | No | Unchanged | Comma-separated tester group names or IDs to grant every build of the app to. Also accepted as app_permission_groups, which is used only when groups is not sent. Replaces existing app-level grants; send none to remove all app-level grants. Build-level grants from testers_groups are not affected. Names that don't resolve are returned in invalid_groups. Combine with notify=on to email them |
testers_groups | No | Unchanged | Comma-separated tester group names or IDs to grant access to this build only. Requires notify (on or 1); ignored otherwise. Adds to existing grants; send none to remove this build's own grants. Names that don't resolve are returned in invalid_build_groups. Also accepted as testers-groups or invitation_groups; if more than one is sent, invitation_groups is used first, then testers-groups |
notify | No | off | Set to on (or 1) to email the app's tester groups about the new build |
symbols_file | No | None | Debug symbols to attach (iOS dSYM or Android mapping file). Also accepted as proguard_file |
sync_to_saucelabs | No | off | Set to on to also copy the build to Sauce Labs App Storage |
upload_to_saucelabs | No | off | Legacy name for sync_to_saucelabs. Only the exact value on copies the build to Sauce Labs App Storage |
community_token | No | Unchanged | URL alias for the app's landing page. 6-63 characters: letters, digits, dot, hyphen, or underscore (error codes 124 invalid, 125 in use) |
landing_page_mode | No | Unchanged | Landing page visibility: open or closed (error code 135 otherwise) |
metadata_* | No | None | Any metadata_<key> field is stored as custom build metadata and returned under metadata |
iOS apps signed with an ad-hoc or development certificate are closed beta unless your organization turns on Allow ad-hoc apps to be open beta in Organization Settings. Without that setting, landing_page_mode=open is ignored for these apps, and the upload still succeeds. A landing page that an upload creates for such an app starts closed; send landing_page_mode=open to open it after the setting is on. If Require login before download is on, every app is closed beta, whatever landing_page_mode says.
Projects
GET /api/1/projects/
List all apps in the organization.
curl -H "X-API-Key: YOUR_KEY" https://app.testfairy.com/api/1/projects/
Response includes: id, self, name, folderName, packageName, platform, icon, landingPageMode.
Builds
GET /api/1/projects/{projectId}/builds/
List all builds for an app.
GET /api/1/projects/{projectId}/builds/{buildId}
Get a single build.
Response includes: id, projectId, appName, appDisplayName, appVersion, appVersionCode, iconUrl, appUrl, platform, comment, tags, downloads, uploadedAt, uploadedVia, isDistributionEnabled.
PATCH /api/1/projects/{projectId}/builds/{buildId}/
Update a build's metadata.
| Parameter | Description |
|---|---|
comment | Update release notes. Also accepted as changelog or release_notes |
tags | Comma-separated tags. Replaces the build's tags |
landing_page_mode | Landing page visibility for the build's app: open or closed (error code 135 otherwise) |
community_token | URL alias for the app's landing page, same rules as on upload (error codes 124 invalid, 125 in use) |
metadata_* | Custom build metadata. Merged with the build's existing metadata, so you can update one key at a time |
landing_page_mode and community_token change the whole app, not just this build. The ad-hoc rules in the /api/upload note above apply to this endpoint too: without Allow ad-hoc apps to be open beta, open is ignored for ad-hoc apps and the request still succeeds.
DELETE /api/1/projects/{projectId}/builds/{buildId}
Delete a build. Requires admin permissions.
GET /api/1/projects/{projectId}/builds/{buildId}/download/
Download a build. Responds with an HTTP 302 redirect to a pre-signed download URL (or to the install page when file storage isn't configured), so follow redirects, for example with curl -L.
POST /api/1/projects/{projectId}/builds/{buildId}/invites/
Send install invitations to testers for a build.
Testers
GET /api/1/testers
List all testers in the organization.
Response includes: id, email, invitationStatus, isBlocked, groups, lastLogin, createdAt, plus hasUdidAccess, allowAll, hasPushToken, emailBounce, onlyAccount, account, allDevices, appleDevices.
POST /api/1/testers/
Add a tester. Creates the user if they don't exist. Requires admin permissions.
| Parameter | Required | Description |
|---|---|---|
email | Yes | Tester's email address |
group | No | Group name to add the tester to |
GET /api/1/testers/{testerId}
Get a single tester's details.
DELETE /api/1/testers/{testerId}
Remove a tester from the organization. Requires admin permissions.
POST /api/1/testers/{testerId}/block/
Block a tester. Requires admin permissions.
DELETE /api/1/testers/{testerId}/block/
Unblock a tester. Requires admin permissions.
Tester Groups
GET /api/1/testers/groups
List all tester groups. Response includes: id, name, testers (a nested list of { "email" } objects).
POST /api/1/testers/groups
Create a tester group. Requires admin permissions.
| Parameter | Required | Description |
|---|---|---|
groupName | Yes | Name for the new group |
POST /api/1/testers/groups/{groupId}
Add an existing tester to a group by email. If the email doesn't belong to a tester in your organization, the call returns { "status": "ok", "testers": [] } and nothing changes. Requires admin permissions.
| Parameter | Required | Description |
|---|---|---|
email | Yes | Tester's email address |
DELETE /api/1/testers/groups/{groupId}
Remove a tester from a group by email. Requires admin permissions.
| Parameter | Required | Description |
|---|---|---|
email | Yes | Tester's email address (POST body or query param) |
Groups
GET /api/1/groups/
List all groups in the organization. Response includes: id, name, private.
GET /api/1/groups/{groupId}
Get a single group.
GET /api/1/groups/{groupId}/testers/
List all testers in a group. Each entry contains email only. Paginated with page and per_page (default 50, max 200).
GET /api/1/groups/{groupId}/projects/
List all apps assigned to a group. Response includes: id, name. Paginated with page and per_page (default 25, max 100).
Webhooks
GET /api/1/webhooks/
List all webhooks for the organization.
Response includes: id, name, url, status, actions (comma-separated), projectIds (comma-separated, or * for all apps).
POST /api/1/webhooks/
Create a webhook. Requires admin permissions.
| Parameter | Required | Description |
|---|---|---|
url | Yes | Webhook callback URL |
name | Yes | Display name. Also accepted as webhook-name. Missing name returns code 104 |
actions | No | Comma-separated event types to listen for |
project_ids | No | Comma-separated app IDs (empty = all apps) |
GET /api/1/webhooks/{webhookId}
Get a single webhook.
POST /api/1/webhooks/{webhookId}
Update a webhook. Requires admin permissions. Accepts the same parameters as create (all optional).
DELETE /api/1/webhooks/{webhookId}
Delete a webhook. Requires admin permissions.
Sites (Teams)
In the legacy API, "sites" correspond to "teams" in the current platform.
GET /api/1/sites/
List all sites (teams) in the organization.
Response is { "site": { "accounts": [...], "managers": [...] } }. Each account includes id, name, buildsCount and users (each with email and role); each manager includes email. Only the account owner or an org admin can call this endpoint; other roles receive { "status": "fail", "code": 1, "message": "Feature is not enabled" } with HTTP 200.
GET /api/1/sites/{siteId}
Get a single site (team).
POST /api/1/sites/
Create a site (team). Requires admin permissions.
| Parameter | Required | Description |
|---|---|---|
name | Yes | Site (team) name |
Audit Logs
Requires admin permissions. GET /api/1/audits/ ignores query parameters and always returns up to 1000 app-download events. The /api/2 audit endpoints support the following query parameters:
| Parameter | Description |
|---|---|
page | Page number (default: 1) |
limit | Results per page (default: 50, max: 100). per_page is also accepted and takes precedence |
action | Filter by action type |
search | Search in email and action data |
from | Start date (ISO 8601) |
to | End date (ISO 8601) |
GET /api/1/audits/
List audit log entries.
GET /api/2/audits/
List audit log entries (v2 format with pagination metadata).
GET /api/2/audits/admin-trail/
List admin activity audit trail.
GET /api/2/audits/tester-trail/
List tester activity audit trail.
Response includes: id, timestamp, enterpriseId, siteName, userId, userEmail, ipAddress, actionType, actionLabel, actionData, plus pagination object.
Error Codes
| Code | HTTP Status | Meaning |
|---|---|---|
1 | 400 | Missing or invalid required parameter |
2 | 400 | Duplicate resource (already exists) |
5 | 401/403 | API key valid but no organization membership (401), or admin permissions required (403). On /api/upload, an invalid key also returns code 5 with HTTP 200 |
104 | 401 | Missing or invalid API key (/api/1 and /api/2 endpoints) |
112 | 400 | Empty file uploaded |
121 | 400 | Invalid file type |
124 | 400 | Invalid community_token |
125 | 400 | community_token already in use by another app |
133 | 400 | Organization not configured (no team found) |
135 | 400 | Unsupported landing_page_mode (use open or closed) |
136 | 400 | team_id required or invalid: you belong to several teams, or the app exists on more than one of them |
150 | 502 | App bundle could not be converted to an installable APK |
400 | 200 | Tester not found (for example, Invalid Tester) |
404 | 200 | Group not found |
CI/CD Examples
Gradle (Android)
curl https://app.testfairy.com/api/upload \
-F api_key=$API_KEY \
-F file=@app/build/outputs/apk/release/app-release.apk \
-F changelog="$(git log -1 --pretty=%B)"
Xcode (iOS)
curl https://app.testfairy.com/api/upload \
-F api_key=$API_KEY \
-F file=@build/MyApp.ipa \
-F changelog="$(git log -1 --pretty=%B)" \
-F notify=on
List apps and builds
# List apps
curl -H "X-API-Key: $API_KEY" https://app.testfairy.com/api/1/projects/
# List builds for an app
curl -H "X-API-Key: $API_KEY" https://app.testfairy.com/api/1/projects/123/builds/
# Get download URL
curl -H "X-API-Key: $API_KEY" https://app.testfairy.com/api/1/projects/123/builds/456/download/
Migration to API v3
To migrate, see the API Migration Guide.