SpawnDroid

API reference

Upload builds, start and manage Android runners, and receive CI events programmatically.

The SpawnDroid API lets a pipeline or a service upload builds, start and manage Android runners, and receive events when work completes. Every request is scoped to a workspace and authenticated with an API key or a signed in console session.

Authentication

Create an API key under Settings and Billing → API keys. The secret is shown once and never stored in plaintext; SpawnDroid keeps only a hash. Send it as a bearer token:

curl https://app.spawndroid.io/api/apps \
  --header "Authorization: Bearer $SPAWNDROID_API_KEY"

Keys carry a scope, enforced on every request. Scopes are a hierarchy: read ⊂ upload ⊂ full.

ScopeAllows
fullEverything, including deleting builds
uploadUpload builds, start and tear down runners, and read
readRead builds, runner history and retained logcat/recordings

Requests from the console authenticate with the signed in session instead of a key, so the same routes work in both contexts. Only workspace admins can create or revoke keys and webhooks.

Base URL and conventions

All routes are served from https://app.spawndroid.io.

  • Requests and responses use JSON, except the multipart upload fallback.
  • Timestamps are ISO 8601 strings.
  • Errors return a JSON body with an error string and an HTTP status. Capacity errors also carry a code.

Builds

POST /api/apps/presign

Mint a short lived presigned PUT so the client uploads the APK straight to object storage. This is the recommended path for CI and for large builds, because it keeps the bytes away from the API function.

Body:

{ "filename": "app-release.apk", "size": 42000000 }

Only .apk files are accepted, up to 512 MB. The response gives you a storage key and a URL:

{
  "key": "org_123/apps/app-release.apk",
  "url": "https://…",
  "contentType": "application/vnd.android.package-archive"
}

Upload the file bytes to url with a PUT, then register the key with POST /api/apps.

POST /api/apps

Register an uploaded build and return the artifact record. Two bodies are accepted.

BodyContent typeWhen to use
{ key, filename, manifest? }application/jsonAfter a presigned upload. Recommended for pipelines.
multipart filemultipart/form-dataSmall builds and local scripts.

The response is { app, parsed, stream }. The app object includes the artifact's globally unique slug. Re uploading a new version of the same package appends to the artifact; re uploading the same version code replaces that release.

curl --request POST https://app.spawndroid.io/api/apps \
  --header "Authorization: Bearer $SPAWNDROID_API_KEY" \
  --form "file=@app-release.apk"

GET /api/apps

List the artifacts in the workspace. Returns { "apps": [ … ] }.

DELETE /api/apps/:slug

Delete an artifact, stop its live runners and remove every retained version from storage.

POST /api/apps/:slug/stream

Start a runner for an artifact and install the selected version.

{ "versionCode": 42, "profileId": "android-14-phone" }

Both fields are optional. The response is { session, package, launched }. When every parallel slot is busy the request returns 429 with code: "concurrency".

GET /api/apps/:slug/session

Return the newest live runner for an artifact, or { "session": null }. This is the endpoint the player polls.

Runners

GET /api/sessions

List the runner history for the workspace.

DELETE /api/sessions/:id

Stop a running session. The console uses this as its kill switch.

POST /api/sessions/:id/heartbeat

Keep a session's device alive while a viewer is connected. Returns { ok }. The console calls this on a timer; a session with no heartbeat is reaped by the device host after a while and returned to the pool.

GET /api/sessions/:id/logcat

Return live logcat lines from the running device. Pass ?lines= to choose how many, clamped between 1 and 5000, defaulting to 300. The response is { available, lines }.

GET /api/sessions/:id/device

Return the live device configuration decoded from the running device.

CI sessions

Hosted runners for pipelines. The full walkthrough, capabilities and a complete pipeline example live in the CI tests guide.

POST /api/v1/sessions

Provision a hosted Android runner and an Appium endpoint. The body names the build to install.

{ "artifactId": "app_1234", "profileId": "android-14-phone" }

artifactId is the id of an uploaded build. The response returns 201 with everything a pipeline needs to build its capabilities:

{
  "sessionId": "sess_1234",
  "state": "booting",
  "webdriverUrl": "https://…/ci/sess_1234/tok_…/wd/hub",
  "device": { "serial": "127.0.0.1:5555", "apiLevel": 34, "osVersion": "14" },
  "app": { "package": "com.example.app" }
}

Provisioning is asynchronous, so poll until state is ready. The WebDriver URL is stable from creation.

GET /api/v1/sessions/:id

Readiness poll. Returns { sessionId, state, error, webdriverUrl, device, package }.

DELETE /api/v1/sessions/:id

Tear down the runner, retain its artifacts and record the outcome.

{ "result": "passed" }

result may be passed or failed and defaults to passed, so a pipeline that never reports an outcome still closes cleanly. The response is { ok, result, logcatStored, videoStored }.

GET /api/v1/sessions/:id/logcat

Download the retained logcat capture. Add ?format=json to receive parsed lines instead of a file.

GET /api/v1/sessions/:id/video

Download the retained screen recording.

Device profiles

GET /api/device-profiles

Return the catalog of device profiles the console picker uses, as { profiles, defaultProfileId }. Pass a profile id as profileId when starting a runner.

Webhooks

Workspace webhooks deliver CI events to an HTTPS endpoint you control. Create one under Settings and Billing → Integrations.

Events

EventFires when
artifact.uploadedA build is stored or replaced
session.startedA runner starts, including CI runners
session.endedA runner finishes, carrying its result and artifacts

Delivery and verification

Each event is a JSON POST with two headers:

  • x-spawndroid-event: the event name.
  • x-spawndroid-signature: sha256= followed by an HMAC of the raw request body, computed with the subscription secret.

The payload contains the event name, the workspace id, a timestamp and a data object whose shape depends on the event.

{
  "event": "session.ended",
  "tenantId": "org_123",
  "timestamp": "2026-10-12T09:30:00.000Z",
  "data": { "sessionId": "sess_1234", "result": "passed" }
}

Recompute the HMAC over the raw body and compare it with the header before trusting the request. Deliveries are best effort, so treat them as a signal to fetch the latest state, not as the only record.

Errors

StatusMeaning
400The request is malformed or a required field is missing
401The key or session is missing or invalid
404The artifact, version or session does not exist in this workspace
413The build exceeds the 512 MB limit
429Every parallel slot is busy, with code: "concurrency"
502The device host could not be reached or the runner failed to boot
503The device host or object storage is not configured

On this page