openapi: 3.0.3 info: title: CLOUSD API version: "1.0" description: | Cloud Pixel devices over REST. Base URL `https://api.clousd.com/v1`. Auth: `Authorization: Bearer cl_live_…` - a key from the dashboard (Team → API keys), scope `read` or `control`, covering only the devices of the account. 60 requests per minute per key (429 + Retry-After). Long operations answer 202 with a job; poll `GET /jobs/{id}` until `state` is `done` or `failed`. Errors are always JSON `{ "error": "...", "message": "..." }`. Webhooks: a key created with a `webhook` URL receives POST JSON `{event, ts, phone, job|incident}` for `job.finished`, `health.problem`, `health.resolved`; header `X-Clousd-Signature` = hex(HMAC-SHA256(sha256(key) hex, body)). servers: - url: https://api.clousd.com/v1 security: - bearer: [] components: securitySchemes: bearer: type: http scheme: bearer schemas: Error: type: object properties: error: { type: string, example: unauthorized } message: { type: string } Exit: type: object properties: type: { type: string, enum: [mobile, residential, isp, datacenter, byo, off, host] } country: { type: string, example: SG } city: { type: string } carrier: { type: string } isp: { type: string } ip: { type: string } timezone: { type: string } locale: { type: string } Device: type: object properties: name: { type: string, example: p2 } model: { type: string, example: Pixel 6a } android: { type: string, example: "13" } state: { type: string, enum: [running, stopped] } since: { type: string, format: date-time } viewers: { type: integer } exit: { $ref: "#/components/schemas/Exit" } Job: type: object properties: id: { type: string } kind: { type: string, example: restart } phone: { type: string } state: { type: string, enum: [running, done, failed] } started: { type: string, format: date-time } done: { type: string, format: date-time } output: { type: string } JobAccepted: type: object properties: ok: { type: boolean } job: { $ref: "#/components/schemas/Job" } Snapshot: type: object properties: id: { type: string, example: v21 } created: { type: string, format: date-time } android: { type: string } match: { type: number, description: "Match with a real Pixel, percent", example: 92.4 } current: { type: boolean } restorable: { type: boolean } Health: type: object properties: ts: { type: integer } measured_ago_s: { type: integer } problems: { type: integer } devices: type: array items: type: object properties: name: { type: string } state: { type: string } ok: { type: boolean } framework: { type: string, enum: [ok, not_responding, frozen_apps, memory_view_broken] } network: { type: string, enum: [ok, exit_unreachable] } sim: { type: string } restarts: { type: integer } responses: Accepted: description: Long operation started content: { application/json: { schema: { $ref: "#/components/schemas/JobAccepted" } } } Error: description: Error content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } paths: /devices: get: summary: List your devices responses: "200": description: Devices content: application/json: schema: type: object properties: devices: { type: array, items: { $ref: "#/components/schemas/Device" } } post: summary: Create a device (dashboard only for now) responses: "501": { $ref: "#/components/responses/Error" } /devices/{name}: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: summary: State of one device responses: "200": description: Device content: { application/json: { schema: { type: object, properties: { device: { $ref: "#/components/schemas/Device" } } } } } "404": { $ref: "#/components/responses/Error" } delete: summary: Delete the device for good (container, data, profiles) - 202 + job responses: "202": { $ref: "#/components/responses/Accepted" } "404": { $ref: "#/components/responses/Error" } /devices/{name}/start: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: { summary: Start, responses: { "202": { $ref: "#/components/responses/Accepted" } } } /devices/{name}/stop: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: { summary: Stop, responses: { "202": { $ref: "#/components/responses/Accepted" } } } /devices/{name}/restart: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: { summary: Restart, responses: { "202": { $ref: "#/components/responses/Accepted" } } } /devices/{name}/screenshot: parameters: - { name: name, in: path, required: true, schema: { type: string } } - { name: w, in: query, required: false, schema: { type: integer, minimum: 40, maximum: 540 }, description: "Width in px: returns a JPEG thumbnail, cached 10 s" } get: summary: Screen right now responses: "200": description: PNG, or JPEG when w is given content: image/png: { schema: { type: string, format: binary } } image/jpeg: { schema: { type: string, format: binary } } /devices/{name}/action: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: summary: Do something on the device without a stream session (open app, URL, tap, swipe, text, key, list apps) requestBody: content: application/json: schema: type: object required: [op] properties: op: { type: string, enum: [open_app, close_app, url, tap, swipe, text, key, installed, screen_text, find_text, tap_text, wait_text, scroll], description: "scroll = one human-like swipe (text: down|up); screen_text = texts on screen; find_text/tap_text/wait_text use `text` (substring, case-insensitive); wait_text polls up to ms/1000 s" } package: { type: string, example: com.instagram.android } url: { type: string } text: { type: string, maxLength: 500 } key: { type: string, description: "home back recents enter power volume_up volume_down tab del menu wake sleep, or a key code" } x: { type: integer } y: { type: integer } x2: { type: integer } y2: { type: integer } ms: { type: integer, description: swipe duration } responses: "200": description: Done content: { application/json: { schema: { type: object, properties: { ok: { type: boolean }, output: { type: string }, packages: { type: array, items: { type: string } }, texts: { type: array, items: { type: string } } } } } } "400": { $ref: "#/components/responses/Error" } "500": { $ref: "#/components/responses/Error" } /devices/{name}/input: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: summary: Touch, key, text, rotate (needs a live stream session) requestBody: content: application/json: schema: type: object properties: t: { type: string, enum: [touch, key, text, rotate] } a: { type: string, description: "touch: down|up|move" } x: { type: number } y: { type: number } p: { type: integer, description: pointer id } code: { type: integer, description: Android key code } s: { type: string, description: text } responses: "200": { description: Applied } "409": { $ref: "#/components/responses/Error" } /devices/{name}/apk: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: summary: Install an APK requestBody: content: multipart/form-data: schema: { type: object, properties: { apk: { type: string, format: binary } } } responses: "200": { description: Installed } /devices/{name}/logs: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: { summary: Last logcat lines, responses: { "200": { description: Lines } } } /devices/{name}/stream: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: summary: WebRTC - SDP offer in, SDP answer out responses: { "200": { description: SDP answer } } /devices/{name}/quality: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: summary: Quality by category (last measurement) responses: "200": description: Categories content: application/json: schema: type: object properties: overall: { type: number } measured_at: { type: string } categories: type: object additionalProperties: type: object properties: { label: { type: string }, score: { type: number, nullable: true } } /devices/{name}/network: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: summary: Current exit responses: "200": description: Exit content: { application/json: { schema: { type: object, properties: { network: { $ref: "#/components/schemas/Exit" } } } } } post: summary: Change the exit (time zone, locale, GPS and SIM follow) requestBody: content: application/json: schema: type: object properties: type: { type: string, enum: [mobile, residential, isp, datacenter, byo, off] } country: { type: string, example: SG } url: { type: string, description: "own proxy: socks5:// or http://" } responses: "202": { $ref: "#/components/responses/Accepted" } /devices/{name}/network/rotate: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] post: { summary: New IP, responses: { "200": { description: Rotated } } } /devices/{name}/snapshots: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: summary: Snapshots of a device responses: "200": description: Snapshots content: application/json: schema: { type: object, properties: { snapshots: { type: array, items: { $ref: "#/components/schemas/Snapshot" } } } } post: summary: Take a snapshot (the device is measured first; one at a time per fleet) responses: "202": { $ref: "#/components/responses/Accepted" } "409": { $ref: "#/components/responses/Error" } /devices/{name}/snapshots/{id}/restore: parameters: - { name: name, in: path, required: true, schema: { type: string } } - { name: id, in: path, required: true, schema: { type: string, example: v20 } } post: summary: Restore a snapshot responses: "202": { $ref: "#/components/responses/Accepted" } "404": { $ref: "#/components/responses/Error" } /groups: post: summary: Run a chain of steps on several devices with a pause between them (202 + job; progress per device in job.output) requestBody: content: application/json: schema: type: object required: [phones, steps] properties: phones: { type: array, items: { type: string }, maxItems: 32 } steps: type: array maxItems: 20 items: type: object properties: op: { type: string, enum: [open_app, close_app, url, tap, swipe, text, key, wait, screenshot, start, stop, restart, tap_text, find_text, wait_text, screen_text, profile_save, profile_prune, browse, scroll, if_text, unless_text], description: "browse = look through the current screen like a human for `seconds` (random scrolls and pauses, sometimes back up); scroll = one human-like scroll, `text` = down or up; if_text/unless_text = go on only if the screen does (not) show `text`, otherwise skip the next `skip` steps" } package: { type: string } url: { type: string } text: { type: string } key: { type: string } x: { type: integer } y: { type: integer } seconds: { type: integer, description: "wait: 1-300; browse: 10-600 (warm-up)" } seconds_max: { type: integer, description: "wait: pause a random time between `seconds` and `seconds_max` (optional, >= seconds, <= 300)" } skip: { type: integer, description: "if_text/unless_text: how many next steps to skip when the screen does not match (1-10, default 1)" } stagger: { type: array, items: { type: integer }, minItems: 2, maxItems: 2, description: "[min, max] seconds between devices" } responses: "202": { $ref: "#/components/responses/Accepted" } "400": { $ref: "#/components/responses/Error" } /usage: get: summary: Minutes of running time per device per day (UTC) for a month parameters: [{ name: month, in: query, required: false, schema: { type: string, example: "2026-09" } }] responses: "200": description: Usage content: { application/json: { schema: { type: object, properties: { month: { type: string }, phones: { type: object, additionalProperties: { type: object, properties: { days: { type: object, additionalProperties: { type: integer } }, total: { type: integer }, today: { type: integer } } } } } } } } /schedules: get: summary: Schedules of your devices responses: "200": { description: Schedules } post: summary: Run a chain of steps every day at a time (UTC), optionally on given weekdays requestBody: content: application/json: schema: type: object required: [phones, steps, at] properties: name: { type: string, maxLength: 80 } phones: { type: array, items: { type: string } } steps: { type: array, items: { type: object } } stagger: { type: array, items: { type: integer } } at: { type: string, example: "09:00", description: "HH:MM UTC" } days: { type: array, items: { type: integer, minimum: 1, maximum: 7 }, description: "1 = Monday … 7 = Sunday; empty = every day" } responses: "201": { description: Created } "400": { $ref: "#/components/responses/Error" } /schedules/{id}/toggle: parameters: [{ name: id, in: path, required: true, schema: { type: string } }] post: { summary: Pause or resume a schedule, responses: { "200": { description: Toggled } } } /schedules/{id}/delete: parameters: [{ name: id, in: path, required: true, schema: { type: string } }] post: { summary: Delete a schedule, responses: { "200": { description: Deleted } } } /incidents: get: summary: Incidents on your devices (last 100, newest first) responses: "200": description: Incidents content: application/json: schema: type: object properties: incidents: type: array items: type: object properties: id: { type: string } phone: { type: string } kind: { type: string, enum: [framework, network, sim] } what: { type: string } started: { type: string, format: date-time } resolved: { type: string, format: date-time } outcome: { type: string } /devices/{name}/identity: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: summary: Current identity - serial, IMEI, IMSI/ICCID, phone number, operator, Bluetooth, Wi-Fi, Android ID responses: "200": { description: Identity, content: { application/json: { schema: { type: object, properties: { identity: { type: object, additionalProperties: { type: string } } } } } } } post: summary: New identity in one call (mode keep/clear-apps/factory) or edit given fields (mode set); the device restarts requestBody: content: application/json: schema: type: object properties: mode: { type: string, enum: [keep, clear-apps, factory, set], default: keep, description: "keep apps and data · clear third-party app data · fresh /data · set = only the given fields" } fields: type: object description: "mode set: serial (14 hex), imei (14-15 digits; check digit computed), msisdn (10-15 digits), ssid (1-32), bssid (aa:bb:cc:dd:ee:ff)" additionalProperties: { type: string } responses: "202": { $ref: "#/components/responses/Accepted" } "400": { $ref: "#/components/responses/Error" } /devices/{name}/profiles: parameters: [{ name: name, in: path, required: true, schema: { type: string } }] get: summary: Profiles of a device (apps, logins and identity as saved states) responses: "200": description: Profiles content: application/json: schema: type: object properties: current: { type: string } profiles: type: array items: type: object properties: id: { type: string, example: 20260908T233903Z-7876 } label: { type: string } created: { type: string, format: date-time } size: { type: integer, description: bytes } post: summary: Save the current state as a profile (device pauses for ~15 s) requestBody: content: { application/json: { schema: { type: object, properties: { label: { type: string, maxLength: 80 } } } } } responses: "202": { $ref: "#/components/responses/Accepted" } /devices/{name}/profiles/{id}/switch: parameters: - { name: name, in: path, required: true, schema: { type: string } } - { name: id, in: path, required: true, schema: { type: string } } - { name: save, in: query, required: false, schema: { type: string, enum: ["0"] }, description: "0 = do not auto-save the current state first" } post: summary: Switch the device into a profile (auto-saves the current state, then restarts; 1-5 min) responses: "202": { $ref: "#/components/responses/Accepted" } /devices/{name}/profiles/{id}/delete: parameters: - { name: name, in: path, required: true, schema: { type: string } } - { name: id, in: path, required: true, schema: { type: string } } post: summary: Delete a profile responses: "200": { description: Deleted } "404": { $ref: "#/components/responses/Error" } /network/check: post: summary: Check a proxy before using it (IP, country, ISP, mobile/datacenter, response time) requestBody: content: { application/json: { schema: { type: object, required: [url], properties: { url: { type: string, example: "socks5://user:pass@host:1080" } } } } } responses: "200": description: Result (ok false with a message when the proxy does not answer) content: { application/json: { schema: { type: object, properties: { ok: { type: boolean }, ip: { type: string }, country: { type: string }, city: { type: string }, isp: { type: string }, mobile: { type: boolean }, hosting: { type: boolean }, ms: { type: integer }, message: { type: string } } } } } /health: get: summary: Health of your devices (measured every 5 minutes) responses: "200": description: Health content: { application/json: { schema: { $ref: "#/components/schemas/Health" } } } "503": { $ref: "#/components/responses/Error" } /jobs: get: summary: Last 50 jobs of your devices responses: "200": description: Jobs content: { application/json: { schema: { type: object, properties: { jobs: { type: array, items: { $ref: "#/components/schemas/Job" } } } } } } /jobs/{id}: parameters: [{ name: id, in: path, required: true, schema: { type: string } }] get: summary: One job responses: "200": description: Job content: { application/json: { schema: { type: object, properties: { job: { $ref: "#/components/schemas/Job" } } } } } "404": { $ref: "#/components/responses/Error" }