Go SDK for the Devicebase device automation API. Control three device platforms programmatically over HTTP:
| Platform | Covers | Path family |
|---|---|---|
| mobile | Android, HarmonyOS, iOS | /v1/{action}/{serialno} |
| browser | Chrome / Chromium / Edge over CDP | /api/browser/{serialno}/{action...} |
| computer | macOS / Windows / Linux desktops | /api/computer/{serialno}/{action} |
| cloud browser | browsers the platform runs for you | /v1/browser/* |
| account | your own profile and daily points | /v1/user/* |
go get github.com/devicebase/go-devicebasepackage main
import (
"fmt"
"log"
devicebase "github.com/devicebase/go-devicebase"
)
func main() {
client := devicebase.NewClient(
devicebase.WithAPIKey("your-api-key"),
devicebase.WithSerialno("db-mttul4i41di8"),
)
info, err := client.GetDeviceInfo()
if err != nil {
log.Fatal(err)
}
fmt.Println("device:", info.Serial)
screenshot, err := client.GetScreenshot()
if err != nil {
log.Fatal(err)
}
fmt.Printf("screenshot: %d bytes\n", len(screenshot))
if _, err := client.Tap(100, 200); err != nil {
log.Fatal(err)
}
if _, err := client.LaunchApp("com.tencent.mm"); err != nil {
log.Fatal(err)
}
}ListDevices is the entry point: it is the only call that needs no serialno, and it is how a serialno is discovered. Filters are resolved server-side.
devices, err := client.ListDevices(devicebase.ListDevicesRequest{
Type: "browser", // category bucket, or a system type such as "macos"
State: "free",
})
for _, device := range devices {
fmt.Println(device.Serialno, device.Type, device.OSType)
}Type accepts either a category bucket (mobile, browser, computer) or a system type (android, harmonyos, ios, macos, windows, linux, chrome, chromium, edge, other). Buckets match against the device's os_type, because a device row only carries the coarse type — a Chrome browser is type=browser with os_type=Chrome. linux means every computer that is neither macOS nor Windows, and other means every browser that is not Chrome/Chromium/Edge.
Limit caps the result (the server defaults to 10, clamped to 1-100).
The device identifier is the serialno field (e.g. db-mttul4i41di8), which the server issues. The device_sn UUID also resolves — the gateway looks devices up with WHERE (serialno = ? OR device_sn = ?) — but serialno is the primary key.
Nothing to control? Create a cloud browser and the SDK hands you one.
client := devicebase.NewClient(
devicebase.WithAPIKey("your-api-key"), // Optional: falls back to DEVICEBASE_API_KEY
devicebase.WithSerialno("db-mttul4i41di8"), // Optional: for the mobile methods
devicebase.WithBaseURL("https://api.devicebase.cn"), // Optional, default shown
devicebase.WithTimeout(30 * time.Second), // Optional, default shown
devicebase.WithHTTPClient(httpClient), // Optional
)Environment variables are used as fallbacks:
DEVICEBASE_API_KEY— API key for authenticationDEVICEBASE_BASE_URL— API base URL (default:https://api.devicebase.cn)
WithSerialno binds a device for the mobile methods only. The browser, computer and list methods take the serialno per call, so one client can drive many devices across platforms:
Renamed:
WithSerial→WithSerialno,Client.Serial()→Client.Serialno(),DeviceInfo.Serial→DeviceInfo.Serialno. The old names are kept as deprecated aliases and are removed in the next major release. Positional arguments are unaffected.
Devicealso accepts the legacyserialkey in a device-list row. The older Python service sends that spelling; without the fallback it decoded to an emptySerialno.
client := devicebase.NewClient(devicebase.WithAPIKey("your-api-key"))
client.BrowserNavigate("db-mtsi49bf0mqb", "https://example.com")
client.ComputerClick("db-mtthisv311f1", devicebase.ComputerClickRequest{X: 640, Y: 360})A missing API key is reported by the first request as an AuthenticationError, not at construction, so a client can be built before the environment is fully set up.
// Touch
client.Tap(100, 200)
client.DoubleTap(100, 200)
client.LongPress(100, 200)
client.Swipe(0, 500, 500, 500)
// Navigation
client.Back()
client.Home()
// Apps
client.LaunchApp("com.tencent.mm")
client.StopApp("com.tencent.mm")
client.StopCurrentApp()
appInfo, _ := client.GetCurrentApp()
// Text
client.InputText("Hello World")
client.ClearText()
// Inspection
info, _ := client.GetDeviceInfo()
hierarchy, _ := client.DumpHierarchy()
// Shell (adb/hdc platforms only)
result, _ := client.Bash("ls -la /sdcard")
fmt.Println(result.Data["exitCode"])
// Install — the path is on the agent host, not local
install, _ := client.InstallApp("/tmp/app.apk")
client.InstallStatus(install.Data["installId"].(string))The serialno of a registered browser device, from ListDevices{Type: "browser"}. Selectors are CSS selectors.
client.BrowserNavigate(serialno, "https://example.com")
client.BrowserRefresh(serialno)
client.BrowserGoBack(serialno)
client.BrowserGoForward(serialno)
client.BrowserClick(serialno, "button#submit")
client.BrowserFill(serialno, "#search", "devicebase")
client.BrowserSelect(serialno, "#country", "CN")
client.BrowserText(serialno, "#search")
client.BrowserAttribute(serialno, "a.logo", "href")
client.BrowserExists(serialno, ".modal")
client.BrowserExecute(serialno, "document.title") // danger tier: runs in the page
client.BrowserInput(serialno, "hello 世界") // CDP insertText, reliable for CJK
client.BrowserHotkey(serialno, []string{"Meta", "a"})
client.BrowserState(serialno)
client.BrowserTabs(serialno)
client.BrowserTabOpen(serialno, "https://example.com")
client.BrowserTabSwitch(serialno, tabID)
client.BrowserTabClose(serialno, tabID)
client.BrowserTabCloseAll(serialno)
client.BrowserLaunch(serialno)
client.BrowserClose(serialno)Editing shortcuts (Meta a, Backspace) act on the page. Browser-chrome shortcuts such as Control t are not reachable — CDP drives the page, not the browser UI.
The methods above drive a browser that already exists. These four build and destroy one — a browser the platform runs for you on its own cluster, over /v1/browser/*. A Chrome you attached yourself is not a cloud browser: it does not count against the quota and cannot be deleted through these calls (Device.IsCloud tells them apart).
quota, err := client.CloudBrowserQuota() // {Limit, Used, Remaining} for this account
created, err := client.CreateCloudBrowser(devicebase.CloudBrowserCreateRequest{
Name: "my-browser", // optional; the server names it otherwise
WindowSize: "1366x768", // optional; "WxH"
// WaitSeconds is how long to wait for it to register before answering
// (0-60; nil = the server's default of 15).
})
// Creation is asynchronous, so the call waits for it: Serialno is the key every
// Browser method takes, and Registered says whether it made it in time.
fmt.Println(created.Serialno, created.AliasName, created.Registered)
// AliasName is the Name you passed, verbatim. created.Name is the platform's own
// identity for the machine ("Browser-<serial[0:8]>") — not your name for it.
// If it did not come up in time the call still succeeded — ask again with the
// device_sn it returned. "Not yet" is a value, never an error, so this is quiet.
if !created.Registered {
status, err := client.CloudBrowserStatus(created.DeviceSN)
fmt.Println(status.Registered)
}
client.BrowserNavigate(created.Serialno, "https://example.com")
err = client.DeleteCloudBrowser(created.Serialno) // irreversibleA cloud browser is always headless — there is no option for it. DeleteCloudBrowser
accepts either the serialno or the DeviceSN from creation, and does not wait
for the machine: the platform queues the reap and the node collects it on its
next heartbeat, so it succeeds even while the node is offline. Calling it as soon
as create returns is fine even when Registered is false.
Failure modes decide whether a retry is worth it. All of them surface as errors carrying the HTTP status (*Error.StatusCode):
| Status | Meaning | Retry? |
|---|---|---|
| 409 | Quota exhausted (create), or the identifier is not a cloud browser (delete) | No |
| 502 | Platform↔node auth failed — signature, key or clock | No; someone has to fix it |
| 503 | No capacity right now, or the node could not be reached | Yes, later |
The account behind the API key — no device involved, and no serialno to pass: the key identifies the account, and it can only ever be your own.
info, err := client.UserInfo()
fmt.Println(info.Username, info.Mobile, info.Credits, info.RegisteredAt)
reward, err := client.UserCheckin()
fmt.Println(reward.Message, reward.CreditsEarned, reward.Credits)UserInfo returns the name (Username), phone, points balance (Credits),
registration date and CanCheckin — whether today's reward is still unclaimed.
UserCheckin claims the daily points: 25 on the first day, +10 per consecutive
day, up to 95 a day. Claiming twice in one day is not an error — the second
call returns AlreadyChecked: true, CreditsEarned: 0 and the platform's own
message ("今日已签到"), with nothing granted. That makes it safe to run from a
daily scheduled task without checking first:
// cron: 7 9 * * * my-checkin
if _, err := client.UserCheckin(); err != nil {
log.Fatal(err)
}The serialno of a registered computer device, from ListDevices{Type: "computer"}. Coordinates are absolute screen pixels.
client.ComputerClick(serialno, devicebase.ComputerClickRequest{X: 640, Y: 360})
client.ComputerClick(serialno, devicebase.ComputerClickRequest{
X: 640, Y: 360, Button: devicebase.ButtonRight, // empty → server default "left"
})
client.ComputerDoubleClick(serialno, devicebase.Point{X: 640, Y: 360})
client.ComputerLongClick(serialno, devicebase.ComputerLongClickRequest{X: 640, Y: 360, Duration: 2})
client.ComputerMove(serialno, devicebase.Point{X: 100, Y: 100})
client.ComputerDrag(serialno, devicebase.Bounds{X1: 100, Y1: 100, X2: 800, Y2: 600})
client.ComputerScroll(serialno, devicebase.ScrollRequest{
Direction: devicebase.ScrollDown,
Amount: 5, // zero → driver default
})
client.ComputerTypeText(serialno, "hello")
client.ComputerPress(serialno, "Enter")
client.ComputerHotkey(serialno, []string{"Control", "Shift", "Escape"})
client.ComputerPosition(serialno)
client.ComputerScreenSize(serialno)
client.ComputerPermissions(serialno)
client.ComputerLaunchApp(serialno, "Visual Studio Code")
// Wait takes MILLISECONDS; bash --timeout takes SECONDS.
client.ComputerWait(serialno, 2000)
client.ComputerBash(serialno, "ls -la", 30)ComputerBash runs on the host machine as the desktop user, unsandboxed, under the platform default shell — treat it as shell access. A non-zero command exit is reported in data.exitCode, not as an error, because the API call itself succeeded:
result, err := client.ComputerBash(serialno, "exit 3", 10)
// err == nil; result.Data["exitCode"] == 3ComputerWait and ComputerBash raise their HTTP deadline per call to cover however long they were asked to block, so a wait past the default 30s is not aborted client-side.
GetScreenshot posts to /v1/screen/{serialno}, which is a cross-family route: the server dispatches it by device type, so it works for mobile, browser and computer serials alike. The server decides the format (JPEG) regardless of any file extension you choose.
data, err := client.GetScreenshot()DownloadScreenshot is an SDK-only extra with no CLI equivalent — it hits GET /v1/screenshot/{serialno} instead.
Two failure layers are surfaced, and the second is easy to miss:
| Layer | Type | Meaning |
|---|---|---|
| Gateway | AuthenticationError, DeviceNotFoundError, ValidationError, Error |
The HTTP status was non-2xx. StatusCode carries it. |
| Business | BusinessError |
HTTP 200, but the envelope carried a non-2xx code. Code carries it. |
import "errors"
_, err := client.BrowserClick(serialno, "#missing")
if err != nil {
var bizErr *devicebase.BusinessError
var authErr *devicebase.AuthenticationError
var notFoundErr *devicebase.DeviceNotFoundError
var valErr *devicebase.ValidationError
switch {
case errors.As(err, &bizErr):
// The action failed in the driver, e.g. no element matched.
fmt.Println("action failed:", bizErr.Code, bizErr.Body)
case errors.As(err, &authErr):
fmt.Println("authentication failed:", authErr.Message)
case errors.As(err, ¬FoundErr):
fmt.Println("device not found:", notFoundErr.Message)
case errors.As(err, &valErr):
fmt.Println("validation error:", valErr.Message)
default:
fmt.Println("error:", err)
}
}The control API reports action failures inside an otherwise successful response — a selector that matches nothing comes back as HTTP 200 with {"code":502,...}. Guarding on the HTTP status alone would treat that as success, which is why the envelope is inspected too.
go test ./... # hermetic: httptest servers, no network
go test -cover ./...A live smoke suite is included and skipped by default. It only performs read-only actions, and discovers serials from the server rather than hard-coding them:
DEVICEBASE_LIVE_TEST=1 \
DEVICEBASE_API_KEY=<key> \
DEVICEBASE_BASE_URL=http://127.0.0.1:8000 \
go test -run TestLive -v ./...MIT