API Reference (v3)
Use the REST API to integrate app distribution into your CI/CD pipeline.
Migrating from TestFairy? See Legacy API (v1) and the API Migration Guide.
Authentication
All API requests require authentication via one of the following methods:
| Method | Example |
|---|---|
X-API-Key header | curl -H "X-API-Key: YOUR_KEY" ... |
Bearer token | curl -H "Authorization: Bearer TOKEN" ... |
api_key form parameter | curl -F api_key=YOUR_KEY ... |
HTTP Basic (email:apiKey) | curl -u you@example.com:YOUR_KEY ... |
| OIDC | curl -H "Authorization: Bearer JWT" -H "X-OIDC-Config-Key: CONFIG_KEY" ... |
Find your API key by clicking the key icon in the top navigation bar. You can exchange it for a short-lived Bearer token via POST /api/v3/auth/token.
Pagination
All list endpoints support pagination via query parameters:
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number |
per_page | 25 | Results per page (max: 100) |
Larger per_page values are capped at 100, except on /api/v3/audits, which returns a validation error instead.
Paginated responses include a pagination object. The list key matches the resource - projects, builds, teams, testers, groups, webhooks, or audits:
{
"projects": [...],
"pagination": {
"page": 1,
"per_page": 25,
"total": 142,
"total_pages": 6
}
}
Endpoints
Authentication
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v3/auth/token | Exchange API key for a 1-hour Bearer token |
Apps
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/projects | List all apps (paginated) |
| GET | /api/v3/projects/{id} | Get an app |
| POST | /api/v3/projects | Create an app |
| PUT | /api/v3/projects/{id} | Update an app |
| DELETE | /api/v3/projects/{id} | Delete an app (admin) |
| GET | /api/v3/projects/{id}/testers | List testers assigned to an app (direct + via groups, deduped) |
| POST | /api/v3/projects/{id}/copy | Copy an app |
Builds
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/projects/{projectId}/builds | List builds for an app (paginated) |
| GET | /api/v3/builds/{id} | Get a build |
| POST | /api/v3/builds/upload | Upload a new build (multipart/form-data) |
| PUT | /api/v3/builds/{id} | Update release notes, tags, and the app's landing page visibility and URL alias |
| GET | /api/v3/builds/{id}/download | Get pre-signed download URL. If storage isn't configured, returns the install-page URL instead. |
| DELETE | /api/v3/builds/{id} | Delete a build (admin) |
| POST | /api/v3/builds/{id}/copy | Duplicate a build within the same app (references the same file) |
| POST | /api/v3/builds/{id}/notify-testers | Queue the new-build email to testers. Returns 202 {"status":"queued"}, or 409 if the build isn't distributable. Requires admin rights on the app. |
| GET | /api/v3/builds/{id}/symbols/download | Get a download URL for the build's symbols file |
Upload Parameters
POST /api/v3/builds/upload takes multipart/form-data:
| Parameter | Required | Default | Description |
|---|---|---|---|
file | Yes | — | The build file itself (.apk, .aab, .ipa, or .zip), sent as a multipart file part, not a text field. A URL or path in a text field is rejected with 400 {"error": "file is required."} |
project_id | See note | — | ID of the app to upload to. When set, team_id is ignored |
team_id | See note | — | ID of the team to upload to. Required unless project_id is given. The build goes to that team's existing app for the package name, or a new app is created there |
folder | No | None | Folder name. Scopes the package-name lookup and is applied to an auto-created app |
groups | No | Unchanged | Comma-separated tester group names or IDs to grant every build of the app to. Replaces existing app-level grants; omit to leave grants unchanged, or send none to remove all app-level grants. Build-level grants from build_groups are not affected. Names that don't resolve are returned in invalid_groups. If none resolve, grants are left unchanged; if some do, only those are applied, so a misspelled group loses its access. Does not send email on its own (see notify). |
build_groups | No | Unchanged | Comma-separated tester group names or IDs to grant access to this build only. Adds to existing grants; a group not listed keeps its access, and omitting the parameter changes nothing. A group that already has app-level access is still granted this build, so it keeps the build if that app-level grant is later removed. Send none (case-insensitive, as the only value) to remove this build's own grants; app-level grants from groups stay. Names that don't resolve are returned in invalid_build_groups. Independent of groups and notify. Ignored for multi-app zip uploads. Replaces legacy testers_groups, testers-groups and invitation_groups |
landing_page_mode | No | Unchanged | Landing page visibility: open or closed |
landing_page_slug | No | Unchanged | URL alias for the app's landing page. 6-63 characters: letters, digits, dot, hyphen, or underscore, not starting or ending with a separator. Returns 409 if the alias is already used by another app |
notify | No | off | Set to on (or 1) to email the app's tester groups about the new build. Granting groups does not notify them on its own. Ignored for builds that can't be installed (for example, generic files) |
release_notes | No | None | Release notes for the build |
symbols_file | No | None | Debug symbols to attach (iOS dSYM or Android mapping file). Best-effort: an invalid symbols file never fails the upload. |
sync_to_saucelabs | No | off | Set to on to also copy the build to Sauce Labs App Storage. Requires a Sauce Labs connection on the organization |
version | No | Detected from the file | Override the version string |
version_code | No | Detected from the file | Override the version code |
Provide either project_id or team_id.
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.
curl -X POST https://your-org.testfairy.com/api/v3/builds/upload \
-H "X-API-Key: $API_KEY" \
-F project_id=$PROJECT_ID \
-F file=@app-release.apk \
-F notify=on
The API does not fetch builds from a URL. Download the file first (for example from your own S3 bucket), then send its contents as the file part.
Update Parameters
PUT /api/v3/builds/{id} takes a JSON body. Omitted fields are left unchanged. Testers can't update builds (403).
| Parameter | Type | Description |
|---|---|---|
release_notes | string | Release notes for the build |
tags | array of strings | Replaces the build's tags |
landing_page_mode | string | Landing page visibility for the build's app: open or closed. Any other value returns 400 |
landing_page_slug | string | URL alias for the app's landing page, same rules as on upload. Also accepted as community_token. Returns 400 if invalid, 409 if another app already uses it |
curl -X PUT https://your-org.testfairy.com/api/v3/builds/$BUILD_ID \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"release_notes": "Bug fixes", "landing_page_mode": "closed"}'
landing_page_mode and landing_page_slug change the whole app, not just this build. The ad-hoc rules in the 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.
Teams
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/teams | List all teams (paginated) |
| GET | /api/v3/teams/{id} | Get a team |
| POST | /api/v3/teams | Create a team (admin) |
| PUT | /api/v3/teams/{id} | Update a team (admin) |
| DELETE | /api/v3/teams/{id} | Delete a team (admin) |
Testers
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/testers | List testers (paginated, searchable) |
| GET | /api/v3/testers/{id} | Get a tester |
| POST | /api/v3/testers | Invite a tester by email (admin). Only the Tester role can be created via the API. |
| DELETE | /api/v3/testers/{id} | Remove a tester (admin) |
| POST | /api/v3/testers/{id}/block | Block a tester (admin) |
| DELETE | /api/v3/testers/{id}/block | Unblock a tester (admin) |
Groups
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/groups | List all groups (paginated) |
| GET | /api/v3/groups/{id} | Get a group with testers and apps |
| POST | /api/v3/groups | Create a group (admin) |
| PUT | /api/v3/groups/{id} | Update a group (admin) |
| DELETE | /api/v3/groups/{id} | Delete a group (admin) |
| POST | /api/v3/groups/{id}/testers | Add tester to group (admin) |
| DELETE | /api/v3/groups/{id}/testers/{userId} | Remove tester from group (admin) |
| GET | /api/v3/groups/{id}/testers | List testers in a group (paginated) |
| GET | /api/v3/groups/{id}/projects | List apps the group has access to (paginated) |
Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/webhooks | List all webhooks (paginated) |
| GET | /api/v3/webhooks/{id} | Get a webhook |
| POST | /api/v3/webhooks | Create a webhook (admin) |
| PUT | /api/v3/webhooks/{id} | Update a webhook (admin) |
| DELETE | /api/v3/webhooks/{id} | Delete a webhook (admin) |
Settings
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/settings/oidc | Get the organization's OIDC configuration (admin) |
| POST | /api/v3/settings/oidc | Create or update the OIDC configuration (admin) |
| DELETE | /api/v3/settings/oidc | Delete the OIDC configuration (admin) |
| POST | /api/v3/settings/oidc/test | Test OIDC discovery against the issuer (admin) |
Audit Logs
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v3/audits | List audit logs (paginated, filterable by action/search/date, admin) |
| GET | /api/v3/audits/actions | List distinct audit action types (admin) |