CLI reference
@betadrop/cli publishes an .ipa or .apk and prints the install link. It is the same client the GitHub Action runs, so a workflow behaves exactly like your laptop. This page is the reference; the CLI overview is the argument for using one.Install
npm install -g @betadrop/cli # requires Node 18 or newer
bd --version # or: betadrop --versionThe package installs two binaries that are the same program: bd for typing and betadrop for scripts, where a two-letter command in someone else's pipeline is a collision waiting to happen. Examples on this page use betadrop for that reason. Node 18 is the floor, which every GitHub-hosted runner image already exceeds — no setup step is needed there.
Authenticate
There are two credentials paths and the choice is really "can a browser open here". On a workstation, run betadrop login and pick a method: Google or email both open your browser to approve the device, and the third option takes an API token pasted in. On a runner, skip login entirely and set BETADROP_TOKEN.
betadrop login # interactive: Google, email, or paste a token
betadrop login --token bd_live_xxxxxxxx # non-interactive, same result
BETADROP_TOKEN=bd_live_xxxxxxxx betadrop … # CI: no stored credential at allBETADROP_TOKEN wins over anything stored on disk, and when it is set the CLI neither reads nor writes the credentials file — which is what makes it safe on a shared runner. It is also why betadrop login tells you it is set and declines to do anything, rather than silently storing a second credential that will never be used.
Otherwise credentials are written to $XDG_CONFIG_HOME/betadrop/config.json when that variable is set, and ~/.betadrop/config.json otherwise — including on Windows. The file is created with owner-only permissions (0600).
betadrop logout revokes the token on the server and then deletes the local file. The order matters: it clears local credentials even if the revoke call fails, so an offline logout still leaves nothing behind on the machine. Create and revoke tokens in the dashboard under Settings → Developer → API tokens.
Commands
| Command | What it does |
|---|---|
| betadrop login | Authenticate. --token <token> logs in non-interactively. Already signed in, it offers to keep the session, switch accounts, or log out first. |
| betadrop logout | Revoke the stored token server-side and delete the local credentials file. |
| betadrop whoami | Print the authenticated account, its role, and the active token's name, expiry and last-used date. --json prints the same as an object. |
| betadrop publish <file> | Upload a build and print its install link. Flags: --name, --notes, --ci, --json — see below. |
| betadrop --version | Print the CLI version. --help lists commands; add it after a command for that command's flags. |
publish
The command everything else exists to support. It takes exactly one file, which must end in .ipa or .apk.
| Flag | Effect |
|---|---|
| --name <name> | Override the build name shown on the install page. Without it, the name is read out of the archive. |
| --notes <notes> | Release notes for this build, shown to the tester on the install page. A commit message is the obvious thing to pass from CI. |
| --ci | Non-interactive: no progress bar, no QR block, no summary. The install URL is the last line of standard output, so capture it with url="$(betadrop publish app.apk --ci | tail -n 1)" — which is exactly what our own GitHub Action does. |
| --json | Print a JSON object instead of the human summary, and suppress the progress bar the same way --ci does. |
Output shapes
Three modes, and picking the wrong one is the usual reason a pipeline ends up parsing a progress bar. Default is for humans: a progress bar while uploading, then the app name and version, the install link, and a QR code drawn in the terminal. --ci reduces that to the URL alone. --json gives you the identifiers as well:
$ betadrop publish ./MyApp.ipa --json
{
"id": "…",
"short_id": "…",
"install_url": "https://betadrop.app/install/?i=…"
}The printed URL uses the ?i= query form. The shorter /install/<id> path form resolves to the same page and neither is deprecated — links already shared in the query form keep working indefinitely, which is the reason both exist.
Two checks before any bytes leave
- Extension. Anything other than
.ipaor.apkis rejected outright. - Magic bytes. IPAs and APKs are both ZIP archives, so the first four bytes must be
PK\x03\x04. This catches the classic mistake — a file renamed to.ipa— locally, instead of after uploading several hundred megabytes for the server to reject.
Paths with spaces
Quote them. Xcode output paths routinely contain spaces, and an unquoted path arrives as several arguments. If rejoining those fragments happens to point at a real file the CLI uses it and warns you to quote it next time; if it does not, you get a quoting hint rather than a baffling "unsupported file type" error.
Environment variables
| Variable | Effect |
|---|---|
| BETADROP_TOKEN | The API token to authenticate with. Takes precedence over stored credentials, and suppresses all reads and writes of the credentials file. This is the CI mechanism. |
| BETADROP_API_URL | Override the API host. Defaults to https://api.betadrop.app. You will not need this unless you have been told you do. |
| BETADROP_APP_URL | Override the host used to build the printed install link. Defaults to https://betadrop.app. |
| XDG_CONFIG_HOME | Standard XDG variable. When set, credentials live at $XDG_CONFIG_HOME/betadrop/config.json instead of ~/.betadrop/config.json. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Runtime or authentication failure — an expired or revoked token, a network error, a rejected upload. |
| 2 | Usage error: a missing file, an unsupported extension, a file that is not really a ZIP archive, or too many arguments from an unquoted path. |
| 130 | Cancelled at an interactive prompt (Ctrl-C). |
The distinction between 1 and 2 is worth wiring into a pipeline: a 2 will fail again on retry because the input is wrong, while a 1 may well be transient.
Using it in CI
# Any runner with Node on it. No interactive login: the token is the credential.
npm install -g @betadrop/cli
export BETADROP_TOKEN="$BETADROP_TOKEN" # from your CI secret store
url="$(betadrop publish ./build/app-release.apk --ci)"
echo "Install: $url"That is the whole pattern, and it is what the GitHub Action does for you — it installs this package and shells out to this command, which is why its behaviour is documented here rather than duplicated there. On GitLab CI, Bitrise, CircleCI or Jenkins, the snippet above is the integration in full.
Two habits worth adopting. Pin the version — npm install -g @betadrop/[email protected] or whichever version you validated — so a pipeline that passed today passes tomorrow. And capture the URL rather than only printing it: posting it to the pull request or the team channel is what turns a green build into something a tester can act on. For worked examples of both, see the GitHub Actions guide and the fastlane guide.