@betadrop/cli
v0.4.0
Published
BetaDrop CLI — publish iOS/Android builds, check they will install, and manage links, devices and webhooks from your terminal or CI.
Readme
@betadrop/cli
Ship a build to a tester in one command. betadrop publish app.ipa uploads an iOS .ipa or
Android .apk to BetaDrop and prints an over-the-air install link plus a
scannable QR code — no TestFlight review wait, no Play track, no tester accounts. Your tester opens
the link on their phone and installs from the browser.
npm install -g @betadrop/cli
betadrop login
betadrop publish ./build/MyApp.ipaUploading MyApp.ipa…
████████████████████ 100% · 24.3/24.3 MB
✓ Published to BetaDrop
MyApp v1.2.0 (ios)
Install link https://betadrop.app/install/?i=abc123
Scan with your phone:
[compact QR code printed here]Hand over the link, or hold up the QR code. That is the whole loop.
When this is the right tool
| Instead of | What changes | | --- | --- | | TestFlight external testing | No Beta App Review between your build and a tester's phone, and no TestFlight app or invite to accept. Ad-hoc signing rules still apply — see Troubleshooting. | | Play internal testing | No Play Console upload, no track, no tester list to keep in sync — you hand out a URL. | | Diawi / similar OTA services | Same idea, scriptable: one command, a QR code in your terminal, and a standing link that never changes. | | Firebase App Distribution | No Firebase project, no tester SDK, no invite acceptance — the link works in a plain mobile browser. |
Both platforms, one command, one account.
Quick Start
Get up and running in under 2 minutes.
Option A — Browser login (recommended)
betadrop login
# → Choose "Continue with Google" or "Continue with Email"
# → A browser tab opens — approve the request
# → A QR code is printed so you can open the approval page on your phone insteadOption B — API token
# 1. Create a token: betadrop.app → Settings → Developer → API tokens → Create token
# 2. Log in with the token
betadrop login --token bd_live_xxxxxxxxxxxxxxxxxxxx
# 3. Publish — the install link and QR code print instantly
betadrop publish ./build/MyApp.ipaThe install link and a scannable QR code are printed in the terminal after every successful
betadrop publish. QR codes are suppressed in--ciand--jsonmodes.
Commands
| Command | Description |
|---------|-------------|
| betadrop login | Authenticate interactively (browser or API token). Already logged in? You'll be asked what to do. |
| betadrop login --token bd_live_… | Non-interactive token login. |
| betadrop whoami | Show the authenticated user, active token name, expiry, and last-used date. --json for machine output. |
| betadrop logout | Revoke the stored token server-side and remove local credentials. No-op when BETADROP_TOKEN is set — see Configuration. |
| betadrop publish <file> | Upload an .ipa or .apk and print its install link + QR code. |
| betadrop inspect <file> | Check a build will install from a link — provisioning profile type and expiry, signing, testOnly/debuggable — without publishing it. Exits 1 on a failing check. |
| betadrop builds list · show <id> · delete <id…> · restore <id> | Build history and cleanup. list takes --platform, --status, --page, --per-page. |
| betadrop links list · point <slug> <build-id> | Standing links: see what each serves, promote an already-uploaded build. |
| betadrop devices list · export · invite | The iOS device registry. export writes the tab-separated file Apple's portal and fastlane register_devices take. |
| betadrop feedback <build-id> | What testers reported on a build. --json to pipe it into an issue tracker. |
| betadrop tokens list · create <name> · revoke <id> | API tokens. create --read-only mints one that can list and inspect but never publish. |
| betadrop webhooks list · add <url> · test · deliveries · enable · disable · remove | Send build events to your own endpoint, Slack or Discord. |
Every read command takes --json.
Tip: You can use
bdandbetadropinterchangeably for all commands (e.g.betadrop login,betadrop publish).
betadrop publish options
| Flag | Description |
|------|-------------|
| --name <name> | Override the build name. |
| --notes <notes> | Release notes for this build. |
| --standing-link <slug> | Point a standing link at this build the moment it is live, so that one URL — and any "Download from BetaDrop" button embedded with it — serves the new build with nothing to click. Pro and Studio plans; the name must already be claimed on your account. A name that cannot be used fails the command before anything is uploaded. |
| --channel <slug> | Deprecated alias for --standing-link, accepted permanently and hidden from --help. If both are given, --standing-link wins. |
| --expires-in-days <n> | Stop the install link working after n days. Clamped to your plan's maximum, and the command says so when it is. |
| --max-downloads <n> | Stop it after n downloads. |
| --max-devices <n> | Stop it after n different devices. Combine any of these three and the link stops at whichever comes first. |
| --no-expiry | Keep the link working until you delete the build (paid plans). |
| --app-version <v> · --build-number <n> · --bundle-id <id> | Override what BetaDrop reads out of the file — useful when CI stamps the build number after packaging. |
| --ci | Non-interactive: no spinner/progress/QR code; the install URL is the last line of stdout. With a standing link, the line above it is channel-url=<url> — that key is unchanged, because released GitHub Action versions parse it. |
| --json | Output { id, short_id, install_url } as JSON (no QR code). With a standing link, adds channel: { id, slug, label, url } — the key keeps its old name so existing parsers keep working. |
If the build uploads but the standing link could not be moved (it was released mid-publish, say), the install URL is still printed, a
warning:goes to stderr, and the command exits non-zero — a pipeline that asked for one must not go green when it did not move.
One link that never changes
Every publish mints a new install link, which means re-sending it to everyone. A standing link is a fixed URL you re-point at each new build:
betadrop publish ./build/MyApp.ipa --standing-link acmebetaSend it once. Every later publish swaps what it installs, and nobody has to be told again. Standing links are a Pro and Studio feature — claim the name in the dashboard first.
Put a download button on your README
Every build's install link can be wrapped in a "Download from BetaDrop" button — one Markdown image link, no script:
[](https://betadrop.app/install/acmebeta?ref=badge)Copy it from the embed panel under the install link (post-upload screen or the build's
Share tab). Point it at a standing link and betadrop publish --standing-link acmebeta
keeps the button current on every publish. Details: https://betadrop.app/download-button/.
Check a build before you ship it
betadrop inspect ./build/MyApp.ipa && betadrop publish ./build/MyApp.ipa --ciinspect reads the build on BetaDrop's servers and throws it away — nothing is stored and it
needs only a read-only token. It fails on the mistakes that otherwise surface as a tester saying
"it won't install": an App Store or expired provisioning profile, a profile for a different
bundle ID, an unsigned or testOnly APK, or a v1-only signature on an app targeting Android 11+.
--strict fails on warnings too (a development profile, debuggable).
Tell Slack, Discord or your own service
betadrop webhooks add https://hooks.slack.com/services/T000/B000/XXXX
betadrop webhooks add https://ci.example.com/betadrop --events build.published,build.expiringSlack and Discord URLs are recognised and get chat messages; anything else receives signed JSON.
Events: build.published, standing_link.updated, build.expiring, build.file_missing and
tester.installed. Payloads and signature verification:
https://betadrop.app/docs/webhooks/.
Register test devices from a script
betadrop devices invite --name "Sam" --email [email protected] # BetaDrop emails the link
betadrop devices export -o devices.txt # Apple's portal / fastlane formatAlready logged in?
Running betadrop login when a session exists shows a prompt:
You are already logged in as [email protected].
? What would you like to do?
❯ Continue as [email protected]
Sign in with a different account
Log out and sign in againChoosing "Sign in with a different account" starts a fresh login flow and replaces the session.
Choosing "Log out and sign in again" revokes the current token first, then logs in fresh.
Configuration
Set these environment variables to override the defaults:
| Variable | Default | Description |
|----------|---------|-------------|
| BETADROP_TOKEN | (none) | API token to authenticate with. Wins over the stored credentials file — use it in CI. |
| BETADROP_API_URL | https://api.betadrop.app | API server URL (for self-hosted). |
| BETADROP_APP_URL | https://betadrop.app | Frontend host used to build install links. |
CI usage (no login required)
BETADROP_TOKEN=bd_live_xxxxxxxxxxxxxxxxxxxx betadrop publish ./build/MyApp.ipa --ciTo keep one link current across every CI publish, claim a standing link once in the dashboard and pass it each time:
BETADROP_TOKEN=bd_live_xxxxxxxxxxxxxxxxxxxx betadrop publish ./build/MyApp.ipa --ci --standing-link acmebeta
# stdout:
# channel-url=https://betadrop.app/install/?i=acmebeta
# https://betadrop.app/install/?i=Ab3xYzWith
BETADROP_TOKENset,betadrop logindoes nothing but print a notice, andbetadrop logoutcannot revoke the token server-side — it only tells you to revoke it in the dashboard. Rotate CI tokens at betadrop.app → Settings → Developer → API tokens.
On GitHub Actions, skip the install step entirely and use
betadrop-app/upload-action — it wraps this
same CLI and puts the link on the run summary and the pull request.
Troubleshooting
betadrop: command not found (or bd: command not found)
Run npm install -g @betadrop/cli and make sure your npm global bin directory is in PATH
(npm prefix -g shows the path — the bin directory is <prefix>/bin, or the prefix itself on Windows).
Your token is invalid, expired, or was revoked
Run betadrop login to authenticate again. If you are in CI, rotate the secret and update BETADROP_TOKEN.
Could not reach BetaDrop
Check your internet connection. If BETADROP_API_URL is set, verify it points at a running server.
Upload stalled — no data for 60s
Your connection dropped mid-upload. The CLI will retry automatically on server errors. Check your network and run again.
QR code looks garbled
Use a modern terminal that renders Unicode block characters: iTerm2, Windows Terminal, Alacritty, or Ghostty. If your terminal can't render them, use the printed URL directly — they carry the same link.
The iOS build downloads on the phone but will not open
An ad-hoc .ipa only installs on devices that were in the provisioning profile when it was
signed — publishing it does not change that. Send the tester the
UDID checker, which reads the UDID off the iPhone itself
(no Mac, no cable), then add the device in the Apple Developer portal and re-sign.
Also from BetaDrop
The same publish step, on the other two surfaces — one account, one set of API tokens:
| | |
|---|---|
| betadrop-app/upload-action | Publish from a GitHub Actions workflow. The install link lands on the run summary and, optionally, in a pull-request comment that updates itself on every push. |
| @betadrop/mcpsource | MCP server — publish builds, list history and manage tokens by asking Claude, Cursor or Copilot, without leaving the editor. |
Requirements
- Node.js 18+
- A BetaDrop account — the free tier needs no card. Retention and file-size ceilings by plan are on the pricing page.
Development
npm install
npm run build # bundle to dist/bd.js
npm run typecheck # type-check without emitting
node dist/bd.js --helpIssues and pull requests: https://github.com/betadrop-app/betadrop-cli. If this saved you a TestFlight round-trip, a ⭐ helps other mobile teams find it.
License
MIT
