Skip to content

HTTP API reference

Everything the dashboard does goes through this API, and an API token reaches all of it. Use it when the CLI, the GitHub Action and the MCP server do not cover what you need — a release script that promotes a tested build, a job that exports the device registry, feedback piped into an issue tracker.

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.

Every request
curl https://api.betadrop.app/api/cli/whoami \
  -H "Authorization: Bearer bd_live_xxxxxxxxxxxxxxxxxxxx"
Token abilities
AbilityCan call
readEvery GET, plus POST /api/inspect, which stores nothing.
publishEverything 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

Every JSON response
// 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-data with the file in a part named file, up to your plan's file size limit.
  • Field names follow each endpoint as listed below. Older endpoints use snake_case, newer ones camelCase; neither will be renamed under you.

Builds

Publish a build
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"
Build endpoints
RequestWhat it does
POST /api/cli/publishUpload 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/buildsPaginated 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}/restoreBring back a retired build that still has its file.
GET /api/builds/{id}/feedbackTester 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.

An App Store–signed IPA
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
  }
}
Checks returned by inspect
CheckFails when
profile_presentiOS: no embedded provisioning profile.
profile_kindiOS: an App Store profile (error) or a development profile (warning). Also reports ad-hoc and enterprise.
profile_expiryiOS: expired (error) or expiring within 14 days (warning).
bundle_id_matches_profileiOS: the profile was issued for a different bundle ID.
signing_certificateAndroid: unsigned (error), or the certificate has expired (warning).
signature_schemeAndroid: v1-only signature on an app targeting API 30 or later.
test_onlyAndroid: android:testOnly is set.
debuggableAndroid: android:debuggable is true (warning).
launcher_activityAndroid: no launcher activity, so no home-screen icon (warning).

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.

Link endpoints
RequestWhat it does
GET /api/channelsStanding links, each with currentBuild and followerCount.
POST /api/channelsClaim 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}/subscribersTesters following the link, confirmed and pending.
DELETE /api/channels/{id}/subscribers/{subscriberId}Remove a follower.
GET /api/builds/{id}/share-linksA build's share links.
POST /api/builds/{id}/share-linkslink_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=acmev3Is 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.

Device endpoints
RequestWhat it does
GET /api/devicesRegistered devices: UDID, label, model, iOS version, tester.
POST /api/devicesAdd one by hand: udid, optional label.
PATCH /api/devices/{id}Rename it.
DELETE /api/devices/{id}Remove it.
GET /api/devices/exportCSV (Device ID,Device Name); ?format=txt for one UDID per line. betadrop devices export also writes Apple's tab-separated form.
GET /api/devices/linksRegistration 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}/resendSend 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

Token and account endpoints
RequestWhat it does
GET /api/cli/whoamiThe account and the token making the request.
GET /api/cli/tokensActive tokens.
POST /api/cli/tokensname, 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/logoutRevoke the token making the request. Allowed on any ability, so a leaked read-only token can always revoke itself.
GET /api/user/storageStorage used against the plan.

Static sites

Static hosting endpoints
RequestWhat it does
GET /api/static-siteYour hosted sites.
POST /api/static-site/deployUpload a .zip or a single .html as file.
GET /api/static-site/subdomain/check?subdomain=acmeIs a subdomain free.
POST /api/static-site/subdomain{"subdomain": "…"} — claim one.
POST /api/static-site/renewPush a free site's expiry out, spending one of the account's free renewals.