REST API

Upload builds from CI and manage links programmatically. Create a key under API keys and send it as Authorization: Bearer od_….

Command-line uploader

The Rust CLI in cli/ wraps this API with a progress bar and prints every link you need.

cargo install --path cli            # from a checkout of this repository
opendrop login --server https://opendr0p.com --key od_…
opendrop upload build/MyApp.ipa --notes "Fixes login" --expires 30
opendrop upload app-release.apk --json | jq -r .data.storage_url
OPENDROP_SERVER=https://opendr0p.com OPENDROP_API_KEY=od_… opendrop upload app.exe -q
POST /api/v1/builds

Upload a build

Multipart form with the package in file. Optional fields:

  • release_notes — text shown on the install page
  • expires_in_days — 1–3650, or never (default 7)
  • password — protect the install link
  • max_downloads — stop serving after N installs
curl -fsS https://opendr0p.com/api/v1/builds \
  -H "Authorization: Bearer $OPENDROP_API_KEY" \
  -F file=@build/MyApp.ipa \
  -F release_notes="$(git log -1 --pretty=%s)" \
  -F expires_in_days=30

Response 201. app_url and latest_download_url are permanent and always point at the newest build of the same bundle id; storage_url links straight to the object in storage (presigned for 7 days, permanent with a public base URL, null on local disk).

{"data": {
  "id": "Ab3dE6gH9k",
  "name": "MyApp",
  "platform": "ios",
  "version": "1.4.0",
  "build_number": "212",
  "install_url": "https://opendr0p.com/i/Ab3dE6gH9k",
  "app_url": "https://opendr0p.com/a/myapp",
  "latest_download_url": "https://opendr0p.com/a/myapp/download",
  "download_url": "https://opendr0p.com/d/Ab3dE6gH9k",
  "storage": "Cloudflare R2",
  "storage_url": "https://builds.example.r2.cloudflarestorage.com/builds/Ab3dE6gH9k/MyApp.ipa?X-Amz-…",
  "storage_url_expires_at": "2026-11-11T10:00:00Z",
  "status": "live",
  "expires_at": "2026-12-04T10:00:00Z"
}}
GET /api/v1/builds?platform=android&q=1.4&limit=50

List builds

Newest first. Filters are optional.

GET /api/v1/builds/:id

Get a build

PATCH /api/v1/builds/:id

Update link settings

JSON or form fields: release_notes, enabled, max_downloads, password, remove_password, expires_in_days.

curl -X PATCH https://opendr0p.com/api/v1/builds/Ab3dE6gH9k \
  -H "Authorization: Bearer $OPENDROP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'
DELETE /api/v1/builds/:id

Delete a build

GET /api/v1/apps

List apps

GET /api/v1/me

Current user

GitHub Actions

- name: Upload to OpenDrop
  run: |
    URL=$(curl -fsS https://opendr0p.com/api/v1/builds \
      -H "Authorization: Bearer ${{ secrets.OPENDROP_API_KEY }}" \
      -F file=@app/build/outputs/apk/release/app-release.apk \
      | jq -r .data.install_url)
    echo "Install: $URL" >> "$GITHUB_STEP_SUMMARY"

Errors

Errors return JSON {"error": "code", "message": "…"} with status 400 (missing file), 401 (bad key), 404, 413 (too large), 422 (invalid package) or 502 (the storage backend rejected the upload).

OpenDrop — self-hosted beta distribution for iOS, Android & Windows.