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.
| Scope | Allows |
|---|---|
full | Everything, including deleting builds |
upload | Upload builds, start and tear down runners, and read |
read | Read 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
errorstring and an HTTP status. Capacity errors also carry acode.
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.
| Body | Content type | When to use |
|---|---|---|
{ key, filename, manifest? } | application/json | After a presigned upload. Recommended for pipelines. |
multipart file | multipart/form-data | Small 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
| Event | Fires when |
|---|---|
artifact.uploaded | A build is stored or replaced |
session.started | A runner starts, including CI runners |
session.ended | A 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
| Status | Meaning |
|---|---|
400 | The request is malformed or a required field is missing |
401 | The key or session is missing or invalid |
404 | The artifact, version or session does not exist in this workspace |
413 | The build exceeds the 512 MB limit |
429 | Every parallel slot is busy, with code: "concurrency" |
502 | The device host could not be reached or the runner failed to boot |
503 | The device host or object storage is not configured |