HTTP API reference
Authentication
Send an API token as a bearer token. Create one under Settings → Developer → API tokens, with betadrop tokens create, or with POST /api/cli/tokens. The base URL is https://api.betadrop.app/api.
curl https://api.betadrop.app/api/cli/whoami \
-H "Authorization: Bearer bd_live_xxxxxxxxxxxxxxxxxxxx"| Ability | Can call |
|---|---|
| read | Every GET, plus POST /api/inspect, which stores nothing. |
| publish | Everything read can, plus every request that changes something: uploads, deletes, links, devices, webhooks. |
| * | Everything. The default for a token created without abilities. |
The check is made on every route by HTTP method, so a read token cannot reach a write by any path. A missing, expired or revoked token answers 401; a valid token without the ability answers 403 — re-authenticating will not help, a different token will.
Responses, errors and limits
// success
{ "success": true, "data": { … }, "message": "optional" }
// failure — `reason` is a stable code to branch on; `error` is for people
{ "success": false, "error": "This token is not allowed to make changes. It needs the 'publish' ability.", "reason": "…" }- Organizations. Send
X-BetaDrop-Org: <organization id>to act inside an organization; a token created inside one acts there without it. Membership is checked on every request. - Rate limits. Per route, per minute — 30 uploads, 30 inspections, 60 build lists. Over the limit answers
429. - Uploads are
multipart/form-datawith the file in a part namedfile, up to your plan's file size limit. - Field names follow each endpoint as listed below. Older endpoints use
snake_case, newer onescamelCase; neither will be renamed under you.
Builds
curl -X POST https://api.betadrop.app/api/cli/publish \
-H "Authorization: Bearer $BETADROP_TOKEN" \
-F "file=@build/MyApp.ipa" \
-F "notes=Fixes the login crash" \
-F "expiry_type=combined" -F "expiry_time_days=14" -F "expiry_device_limit=25" \
-F "channel=acmebeta"| Request | What it does |
|---|---|
| POST /api/cli/publish | Upload an .ipa or .apk. Fields: file, and optionally name, notes, version, buildNumber, bundleId, expiry_type (time | downloads | devices | combined | none), expiry_time_days, expiry_download_limit, expiry_device_limit, and channel (a standing-link slug to point at the new build). Returns id, shortId, url, and retentionClamp, duplicateOf, channel, channelWarning when they apply. |
| GET /api/cli/builds | Paginated list: page, per_page (max 100), platform, status (active | expired | disabled | deprecated | latest). |
| GET /api/builds/{id} | One build, every column. |
| PATCH /api/builds/{id} | Change name, notes, is_enabled, is_latest, is_deprecated, the expiry fields above, and the install-page branding fields. Other fields are ignored. |
| DELETE /api/builds/{id} | Delete a build. Its install link stops working. |
| POST /api/builds/bulk-delete | {"ids": [...]}. |
| POST /api/builds/{id}/restore | Bring back a retired build that still has its file. |
| GET /api/builds/{id}/feedback | Tester feedback: type, title, description, severity, rating, reporter, device info. |
Inspect a build
POST /api/inspect takes the same file part as a publish, reads it, and throws it away — nothing is stored, so a read token is enough. installableFromLink is false exactly when a check has level error. .aab files are refused with aab_not_installable: an app bundle cannot be installed from a link.
curl -X POST https://api.betadrop.app/api/inspect \
-H "Authorization: Bearer $BETADROP_TOKEN" \
-F "file=@build/MyApp.ipa"
{
"success": true,
"data": {
"platform": "ios",
"installableFromLink": false,
"checks": [
{ "id": "profile_kind", "level": "error",
"message": "Signed with an App Store profile, which installs only through the App Store or TestFlight. …" },
{ "id": "profile_expiry", "level": "ok", "message": "Profile valid until Jan 1, 2027." }
],
"app": { "name": "Acme", "bundleId": "com.acme.app", "version": "2.4.0", "buildNumber": "119", … },
"ios": { "profile": { "kind": "app-store", "teamId": "ABCDE12345", "expiresAt": "2027-01-01T00:00:00Z", … } },
"android": null,
"sha256": "…", "fileSize": 48213904
}
}| Check | Fails when |
|---|---|
| profile_present | iOS: no embedded provisioning profile. |
| profile_kind | iOS: an App Store profile (error) or a development profile (warning). Also reports ad-hoc and enterprise. |
| profile_expiry | iOS: expired (error) or expiring within 14 days (warning). |
| bundle_id_matches_profile | iOS: the profile was issued for a different bundle ID. |
| signing_certificate | Android: unsigned (error), or the certificate has expired (warning). |
| signature_scheme | Android: v1-only signature on an app targeting API 30 or later. |
| test_only | Android: android:testOnly is set. |
| debuggable | Android: android:debuggable is true (warning). |
| launcher_activity | Android: no launcher activity, so no home-screen icon (warning). |
Standing links and share links
The API still calls standing links channels. A standing link is one URL you point at any build; a share link is a per-build link with its own password, label and limits.
| Request | What it does |
|---|---|
| GET /api/channels | Standing links, each with currentBuild and followerCount. |
| POST /api/channels | Claim one: slug, optional label. Pro and Studio. |
| PUT /api/channels/{id}/build | {"buildId": "…"} — point it at a build. Receivers get standing_link.updated; followers are emailed. |
| PATCH /api/channels/{id} | Change the label. The slug cannot change. |
| DELETE /api/channels/{id} | Release the slug. Everyone holding the URL loses it. |
| GET /api/channels/{id}/subscribers | Testers following the link, confirmed and pending. |
| DELETE /api/channels/{id}/subscribers/{subscriberId} | Remove a follower. |
| GET /api/builds/{id}/share-links | A build's share links. |
| POST /api/builds/{id}/share-links | link_type (beta_tester | qa | stakeholder | reviewer), optional label, max_uses, expires_at. |
| PATCH /api/share-links/{id}/short-id | {"short_id": "acmev3"} — a custom short link. Pro and Studio. |
| PATCH /api/share-links/{id}/password | {"password": "…"} to set, null to remove. |
| GET /api/short-id/check?value=acmev3 | Is a short id or slug free. Pro and Studio. |
iOS devices
The UDID registry behind ad-hoc provisioning: collect devices with a link, then export them for Apple's portal or fastlane.
| Request | What it does |
|---|---|
| GET /api/devices | Registered devices: UDID, label, model, iOS version, tester. |
| POST /api/devices | Add one by hand: udid, optional label. |
| PATCH /api/devices/{id} | Rename it. |
| DELETE /api/devices/{id} | Remove it. |
| GET /api/devices/export | CSV (Device ID,Device Name); ?format=txt for one UDID per line. betadrop devices export also writes Apple's tab-separated form. |
| GET /api/devices/links | Registration links and their state. |
| POST /api/devices/links | {"kind": "invite", "tester_name": "…", "tester_email": "…"} emails a personal link; {"kind": "open"} returns the account's shareable one. Optional app_name. |
| POST /api/devices/links/{id}/resend | Send an invite again. |
Webhooks
/api/webhooks — list, add, change, test, rotate the secret, read the delivery log, redeliver. Events, payloads and signature verification are on the webhooks page.
Tokens and account
| Request | What it does |
|---|---|
| GET /api/cli/whoami | The account and the token making the request. |
| GET /api/cli/tokens | Active tokens. |
| POST /api/cli/tokens | name, optional abilities (["read"], ["publish"], ["*"]) and expires_in_days (1–365). Returns the token once. |
| DELETE /api/cli/tokens/{id} | Revoke a token. |
| POST /api/cli/logout | Revoke the token making the request. Allowed on any ability, so a leaked read-only token can always revoke itself. |
| GET /api/user/storage | Storage used against the plan. |
Static sites
| Request | What it does |
|---|---|
| GET /api/static-site | Your hosted sites. |
| POST /api/static-site/deploy | Upload a .zip or a single .html as file. |
| GET /api/static-site/subdomain/check?subdomain=acme | Is a subdomain free. |
| POST /api/static-site/subdomain | {"subdomain": "…"} — claim one. |
| POST /api/static-site/renew | Push a free site's expiry out, spending one of the account's free renewals. |