@xenon-device-management/xenon 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/lib/package.json +2 -2
  2. package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
  6. package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
  9. package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
  10. package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
  15. package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
  16. package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
  17. package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
  18. package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
  19. package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
  21. package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
  24. package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
  25. package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
  26. package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
  27. package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
  29. package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
  30. package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
  31. package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
  32. package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
  33. package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
  34. package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
  35. package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
  36. package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
  37. package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
  38. package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
  39. package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
  40. package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
  41. package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
  42. package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
  43. package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
  44. package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
  45. package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
  46. package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
  47. package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
  48. package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
  49. package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
  50. package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
  51. package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
  52. package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
  53. package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
  54. package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
  55. package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
  56. package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
  57. package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
  58. package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
  59. package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
  60. package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
  61. package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
  62. package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
  63. package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
  64. package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
  65. package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
  66. package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
  67. package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
  68. package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
  69. package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
  70. package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
  71. package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
  72. package/lib/public/index.html +1 -1
  73. package/lib/src/app/apiErrors.js +118 -0
  74. package/lib/src/app/index.js +6 -1
  75. package/lib/src/app/openapi/control.yaml +3129 -0
  76. package/lib/src/app/openapi/grid.yaml +2295 -0
  77. package/lib/src/app/openapi/identity.yaml +2168 -0
  78. package/lib/src/app/openapi/platform.yaml +2885 -0
  79. package/lib/src/app/openapi/sessions.yaml +3784 -0
  80. package/lib/src/app/routers/bug-report.js +4 -1
  81. package/lib/src/app/routers/config.js +6 -107
  82. package/lib/src/app/routers/control.js +117 -59
  83. package/lib/src/app/routers/dashboard.js +13 -7
  84. package/lib/src/app/routers/grid.js +54 -14
  85. package/lib/src/app/routers/profile.js +27 -13
  86. package/lib/src/app/routers/recordings.js +11 -5
  87. package/lib/src/app/routers/reservation.js +63 -15
  88. package/lib/src/app/routers/users.js +4 -0
  89. package/lib/src/app/routers/webhook.js +17 -8
  90. package/lib/src/app/swagger.js +259 -177
  91. package/lib/src/data-service/device-service.js +4 -1
  92. package/lib/src/data-service/deviceFieldOwners.js +1 -0
  93. package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
  94. package/lib/src/device-managers/ios/WDAClient.js +32 -32
  95. package/lib/src/generated/client/edge.js +4 -3
  96. package/lib/src/generated/client/index-browser.js +1 -0
  97. package/lib/src/generated/client/index.d.ts +38 -0
  98. package/lib/src/generated/client/index.js +4 -3
  99. package/lib/src/generated/client/package.json +1 -1
  100. package/lib/src/generated/client/schema.prisma +2 -0
  101. package/lib/src/generated/client/wasm.js +1 -0
  102. package/lib/src/middleware/csrfMiddleware.js +13 -5
  103. package/lib/src/middleware/rateLimitMiddleware.js +19 -5
  104. package/lib/src/middleware/roleGuard.js +20 -0
  105. package/lib/src/services/AIService.js +13 -3
  106. package/lib/src/services/NotificationService.js +28 -28
  107. package/lib/src/services/bug-report/BugReportService.js +8 -2
  108. package/lib/src/services/lease/LeaseService.js +71 -20
  109. package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
  110. package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
  111. package/lib/test/helpers/expressRoutes.js +41 -0
  112. package/lib/test/integration/team-visibility-control.spec.js +2 -2
  113. package/lib/test/unit/access-scopes.spec.js +227 -0
  114. package/lib/test/unit/api-error-handling.spec.js +179 -0
  115. package/lib/test/unit/bug-report/route.spec.js +29 -0
  116. package/lib/test/unit/bug-report/service.spec.js +30 -0
  117. package/lib/test/unit/control-honest-answers.spec.js +134 -0
  118. package/lib/test/unit/device-allocation-routes.spec.js +232 -0
  119. package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
  120. package/lib/test/unit/install-repository-app-team.spec.js +4 -1
  121. package/lib/test/unit/lease/LeaseService.spec.js +5 -4
  122. package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
  123. package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
  124. package/lib/test/unit/omni-vision-failures.spec.js +81 -0
  125. package/lib/test/unit/openapi-coverage.spec.js +114 -0
  126. package/lib/test/unit/profile-router.test.js +42 -0
  127. package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
  128. package/lib/test/unit/recording-orchestrator.spec.js +80 -0
  129. package/lib/test/unit/recordings-library-routes.spec.js +37 -0
  130. package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
  131. package/lib/test/unit/reset-link.test.js +2 -1
  132. package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
  133. package/lib/test/unit/users-router.test.js +14 -1
  134. package/lib/test/unit/wda-client-failures.spec.js +90 -0
  135. package/lib/test/unit/webhook-delivery.spec.js +142 -0
  136. package/lib/tsconfig.tsbuildinfo +1 -1
  137. package/package.json +2 -2
  138. package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
  139. package/prisma/schema.prisma +2 -0
  140. package/lib/src/app/swagger-docs.js +0 -1701
@@ -0,0 +1,3129 @@
1
+ # Control: drive one device.
2
+ # Every route here sits behind the /control guards, in this order: role MEMBER,
3
+ # the devices scope on mutations, the team guard (a hidden device answers as
4
+ # unknown), the ownership guard (409), and on a hub the node-phone gate.
5
+ paths:
6
+ /api/control/{udid}/tap:
7
+ post:
8
+ tags:
9
+ - Control
10
+ parameters:
11
+ - $ref: '#/components/parameters/ControlUdid'
12
+ operationId: tapDevice
13
+ summary: Tap the screen
14
+ description: >-
15
+ Taps the device screen once at (`x`, `y`), in the device's screen points (the `screenWidth`
16
+ × `screenHeight` the device list reports). Android taps through `adb shell input tap`; iOS
17
+ through WebDriverAgent.
18
+
19
+
20
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
21
+ the device (their live preview or recording) or runs an Appium session on it; admins are
22
+ exempt, and your own Appium session on it stays controllable.
23
+
24
+
25
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
26
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
27
+ `501`.
28
+ requestBody:
29
+ required: true
30
+ content:
31
+ application/json:
32
+ schema:
33
+ $ref: '#/components/schemas/ControlPoint'
34
+ example:
35
+ x: 196
36
+ 'y': 420
37
+ responses:
38
+ '200':
39
+ description: Tapped.
40
+ content:
41
+ application/json:
42
+ schema:
43
+ $ref: '#/components/schemas/Success'
44
+ example:
45
+ success: true
46
+ '400':
47
+ description: This server can't do this on the device's platform, or the udid is malformed.
48
+ content:
49
+ application/json:
50
+ schema:
51
+ $ref: '#/components/schemas/Error'
52
+ examples:
53
+ invalidUdid:
54
+ summary: The udid segment is not valid percent-encoding
55
+ value:
56
+ success: false
57
+ error: invalid_udid
58
+ notSupported:
59
+ summary: No device manager for the platform, or it can't do this
60
+ value:
61
+ error: not_supported
62
+ message: Manager not found or tap not supported
63
+ '401':
64
+ $ref: '#/components/responses/Unauthorized'
65
+ '403':
66
+ $ref: '#/components/responses/ControlForbiddenMutation'
67
+ '404':
68
+ $ref: '#/components/responses/ControlDeviceNotFound'
69
+ '409':
70
+ $ref: '#/components/responses/ControlHeld'
71
+ '429':
72
+ $ref: '#/components/responses/RateLimited'
73
+ '500':
74
+ description: The tap failed on the device.
75
+ content:
76
+ application/json:
77
+ schema:
78
+ $ref: '#/components/schemas/Error'
79
+ example:
80
+ error: 'Command failed: adb -s R5CT32ABCDE shell input tap 196 420'
81
+ '501':
82
+ $ref: '#/components/responses/ControlCloudPhone'
83
+ '502':
84
+ $ref: '#/components/responses/ControlNodeUnreachable'
85
+ '503':
86
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
87
+ '504':
88
+ $ref: '#/components/responses/ControlNodeTimeout'
89
+ /api/control/{udid}/swipe:
90
+ post:
91
+ tags:
92
+ - Control
93
+ parameters:
94
+ - $ref: '#/components/parameters/ControlUdid'
95
+ operationId: swipeDevice
96
+ summary: Swipe across the screen
97
+ description: >-
98
+ Swipes from (`x`, `y`) to (`endX`, `endY`) over `duration` milliseconds, in screen points.
99
+
100
+
101
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
102
+ the device (their live preview or recording) or runs an Appium session on it; admins are
103
+ exempt, and your own Appium session on it stays controllable.
104
+
105
+
106
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
107
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
108
+ `501`.
109
+ requestBody:
110
+ required: true
111
+ content:
112
+ application/json:
113
+ schema:
114
+ $ref: '#/components/schemas/ControlSwipeRequest'
115
+ example:
116
+ x: 196
117
+ 'y': 700
118
+ endX: 196
119
+ endY: 200
120
+ duration: 300
121
+ responses:
122
+ '200':
123
+ description: Swiped.
124
+ content:
125
+ application/json:
126
+ schema:
127
+ $ref: '#/components/schemas/Success'
128
+ example:
129
+ success: true
130
+ '400':
131
+ description: This server can't do this on the device's platform, or the udid is malformed.
132
+ content:
133
+ application/json:
134
+ schema:
135
+ $ref: '#/components/schemas/Error'
136
+ examples:
137
+ invalidUdid:
138
+ summary: The udid segment is not valid percent-encoding
139
+ value:
140
+ success: false
141
+ error: invalid_udid
142
+ notSupported:
143
+ summary: No device manager for the platform, or it can't do this
144
+ value:
145
+ error: not_supported
146
+ message: Manager not found or swipe not supported
147
+ '401':
148
+ $ref: '#/components/responses/Unauthorized'
149
+ '403':
150
+ $ref: '#/components/responses/ControlForbiddenMutation'
151
+ '404':
152
+ $ref: '#/components/responses/ControlDeviceNotFound'
153
+ '409':
154
+ $ref: '#/components/responses/ControlHeld'
155
+ '429':
156
+ $ref: '#/components/responses/RateLimited'
157
+ '500':
158
+ description: The swipe failed on the device.
159
+ content:
160
+ application/json:
161
+ schema:
162
+ $ref: '#/components/schemas/Error'
163
+ example:
164
+ error: 'WDA request failed: socket hang up'
165
+ '501':
166
+ $ref: '#/components/responses/ControlCloudPhone'
167
+ '502':
168
+ $ref: '#/components/responses/ControlNodeUnreachable'
169
+ '503':
170
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
171
+ '504':
172
+ $ref: '#/components/responses/ControlNodeTimeout'
173
+ /api/control/{udid}/touchAndHold:
174
+ post:
175
+ tags:
176
+ - Control
177
+ parameters:
178
+ - $ref: '#/components/parameters/ControlUdid'
179
+ operationId: touchAndHoldDevice
180
+ summary: Touch and hold a point
181
+ description: >-
182
+ Presses (`x`, `y`) for `duration` milliseconds (a long press). On Android it is a
183
+ zero-length swipe through `adb`.
184
+
185
+
186
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
187
+ the device (their live preview or recording) or runs an Appium session on it; admins are
188
+ exempt, and your own Appium session on it stays controllable.
189
+
190
+
191
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
192
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
193
+ `501`.
194
+ requestBody:
195
+ required: true
196
+ content:
197
+ application/json:
198
+ schema:
199
+ $ref: '#/components/schemas/ControlTouchAndHoldRequest'
200
+ example:
201
+ x: 196
202
+ 'y': 420
203
+ duration: 1500
204
+ responses:
205
+ '200':
206
+ description: Held and released.
207
+ content:
208
+ application/json:
209
+ schema:
210
+ $ref: '#/components/schemas/Success'
211
+ example:
212
+ success: true
213
+ '400':
214
+ description: This server can't do this on the device's platform, or the udid is malformed.
215
+ content:
216
+ application/json:
217
+ schema:
218
+ $ref: '#/components/schemas/Error'
219
+ examples:
220
+ invalidUdid:
221
+ summary: The udid segment is not valid percent-encoding
222
+ value:
223
+ success: false
224
+ error: invalid_udid
225
+ notSupported:
226
+ summary: No device manager for the platform, or it can't do this
227
+ value:
228
+ error: not_supported
229
+ message: Manager not found or touchAndHold not supported
230
+ '401':
231
+ $ref: '#/components/responses/Unauthorized'
232
+ '403':
233
+ $ref: '#/components/responses/ControlForbiddenMutation'
234
+ '404':
235
+ $ref: '#/components/responses/ControlDeviceNotFound'
236
+ '409':
237
+ $ref: '#/components/responses/ControlHeld'
238
+ '429':
239
+ $ref: '#/components/responses/RateLimited'
240
+ '500':
241
+ description: The long press failed on the device.
242
+ content:
243
+ application/json:
244
+ schema:
245
+ $ref: '#/components/schemas/Error'
246
+ example:
247
+ error: 'WDA request failed: socket hang up'
248
+ '501':
249
+ $ref: '#/components/responses/ControlCloudPhone'
250
+ '502':
251
+ $ref: '#/components/responses/ControlNodeUnreachable'
252
+ '503':
253
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
254
+ '504':
255
+ $ref: '#/components/responses/ControlNodeTimeout'
256
+ /api/control/{udid}/text:
257
+ post:
258
+ tags:
259
+ - Control
260
+ parameters:
261
+ - $ref: '#/components/parameters/ControlUdid'
262
+ operationId: typeTextOnDevice
263
+ summary: Type text into the focused field
264
+ description: >-
265
+ Types `text` into whatever has focus on the device. Android sends it through `adb shell
266
+ input text` (spaces become `%s`), so characters the shell or `input` treat specially may not
267
+ arrive as typed; iOS types through WebDriverAgent.
268
+
269
+
270
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
271
+ the device (their live preview or recording) or runs an Appium session on it; admins are
272
+ exempt, and your own Appium session on it stays controllable.
273
+
274
+
275
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
276
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
277
+ `501`.
278
+ requestBody:
279
+ required: true
280
+ content:
281
+ application/json:
282
+ schema:
283
+ $ref: '#/components/schemas/ControlTextRequest'
284
+ example:
285
+ text: qa.user@example.com
286
+ responses:
287
+ '200':
288
+ description: Typed.
289
+ content:
290
+ application/json:
291
+ schema:
292
+ $ref: '#/components/schemas/Success'
293
+ example:
294
+ success: true
295
+ '400':
296
+ description: This server can't do this on the device's platform, or the udid is malformed.
297
+ content:
298
+ application/json:
299
+ schema:
300
+ $ref: '#/components/schemas/Error'
301
+ examples:
302
+ invalidUdid:
303
+ summary: The udid segment is not valid percent-encoding
304
+ value:
305
+ success: false
306
+ error: invalid_udid
307
+ notSupported:
308
+ summary: No device manager for the platform, or it can't do this
309
+ value:
310
+ error: not_supported
311
+ message: Manager not found or typeText not supported
312
+ '401':
313
+ $ref: '#/components/responses/Unauthorized'
314
+ '403':
315
+ $ref: '#/components/responses/ControlForbiddenMutation'
316
+ '404':
317
+ $ref: '#/components/responses/ControlDeviceNotFound'
318
+ '409':
319
+ $ref: '#/components/responses/ControlHeld'
320
+ '429':
321
+ $ref: '#/components/responses/RateLimited'
322
+ '500':
323
+ description: Typing failed on the device.
324
+ content:
325
+ application/json:
326
+ schema:
327
+ $ref: '#/components/schemas/Error'
328
+ example:
329
+ error: 'Command failed: adb -s R5CT32ABCDE shell input text qa.user@example.com'
330
+ '501':
331
+ $ref: '#/components/responses/ControlCloudPhone'
332
+ '502':
333
+ $ref: '#/components/responses/ControlNodeUnreachable'
334
+ '503':
335
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
336
+ '504':
337
+ $ref: '#/components/responses/ControlNodeTimeout'
338
+ /api/control/{udid}/keyevent:
339
+ post:
340
+ tags:
341
+ - Control
342
+ parameters:
343
+ - $ref: '#/components/parameters/ControlUdid'
344
+ operationId: pressDeviceKey
345
+ summary: Press a key or hardware button
346
+ description: >-
347
+ Presses one key.
348
+
349
+ - **Android:** `keyCode` is an Android key code, passed to `adb shell input keyevent` as
350
+ given: a number (`4` Back, `3` Home, `66` Enter, `67` Backspace) or a name such as
351
+ `KEYCODE_BACK`.
352
+
353
+ - **iOS:** `keyCode` is a name, case-insensitive. `home` (or `3`), `volumeUp`/`volume_up`
354
+ and `volumeDown`/`volume_down` press hardware buttons; `enter`, `backspace`, `delete`, `tab`
355
+ and `escape` type that key. Anything else is tried as a WebDriverAgent button name; one
356
+ the iPhone doesn't have answers `400 unsupported_key`. A hardware button or keyboard key
357
+ whose WebDriverAgent call fails (after the fallback: `/wda/homescreen` for `home`,
358
+ `/wda/type` for a keyboard key) answers `500`, and so does any key while WebDriverAgent
359
+ can't be reached.
360
+
361
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
362
+ the device (their live preview or recording) or runs an Appium session on it; admins are
363
+ exempt, and your own Appium session on it stays controllable.
364
+
365
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
366
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
367
+ `501`.
368
+ requestBody:
369
+ required: true
370
+ content:
371
+ application/json:
372
+ schema:
373
+ $ref: '#/components/schemas/ControlKeyEventRequest'
374
+ example:
375
+ keyCode: 4
376
+ responses:
377
+ '200':
378
+ description: Pressed.
379
+ content:
380
+ application/json:
381
+ schema:
382
+ $ref: '#/components/schemas/Success'
383
+ example:
384
+ success: true
385
+ '400':
386
+ description: >-
387
+ On iOS, a key the iPhone has no equivalent for; this server can't do this on the
388
+ device's platform; or the udid is malformed.
389
+ content:
390
+ application/json:
391
+ schema:
392
+ $ref: '#/components/schemas/Error'
393
+ examples:
394
+ invalidUdid:
395
+ summary: The udid segment is not valid percent-encoding
396
+ value:
397
+ success: false
398
+ error: invalid_udid
399
+ unsupportedKey:
400
+ summary: A key the iPhone has no equivalent for (iOS)
401
+ value:
402
+ error: unsupported_key
403
+ message: The key "f13" isn't available on an iPhone.
404
+ notSupported:
405
+ summary: No device manager for the platform, or it can't do this
406
+ value:
407
+ error: not_supported
408
+ message: Manager not found or pressKey not supported
409
+ '401':
410
+ $ref: '#/components/responses/Unauthorized'
411
+ '403':
412
+ $ref: '#/components/responses/ControlForbiddenMutation'
413
+ '404':
414
+ $ref: '#/components/responses/ControlDeviceNotFound'
415
+ '409':
416
+ $ref: '#/components/responses/ControlHeld'
417
+ '429':
418
+ $ref: '#/components/responses/RateLimited'
419
+ '500':
420
+ description: >-
421
+ The key press failed on the device (on iOS, a hardware button or keyboard key whose
422
+ WebDriverAgent call and fallback both failed, or WebDriverAgent couldn't be reached).
423
+ content:
424
+ application/json:
425
+ schema:
426
+ $ref: '#/components/schemas/Error'
427
+ examples:
428
+ android:
429
+ summary: Android
430
+ value:
431
+ error: 'Command failed: adb -s R5CT32ABCDE shell input keyevent 4'
432
+ ios:
433
+ summary: iOS
434
+ value:
435
+ error: 'WDA request failed: socket hang up'
436
+ '501':
437
+ $ref: '#/components/responses/ControlCloudPhone'
438
+ '502':
439
+ $ref: '#/components/responses/ControlNodeUnreachable'
440
+ '503':
441
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
442
+ '504':
443
+ $ref: '#/components/responses/ControlNodeTimeout'
444
+ /api/control/{udid}/lock:
445
+ post:
446
+ tags:
447
+ - Control
448
+ parameters:
449
+ - $ref: '#/components/parameters/ControlUdid'
450
+ operationId: lockDevice
451
+ summary: Lock the screen
452
+ description: >-
453
+ Locks the device. Android presses the power key (key code 26), which locks a lit screen and
454
+ wakes a dark one. iOS locks through WebDriverAgent, and a WebDriverAgent failure answers
455
+ `500`.
456
+
457
+
458
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
459
+ the device (their live preview or recording) or runs an Appium session on it; admins are
460
+ exempt, and your own Appium session on it stays controllable.
461
+
462
+
463
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
464
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
465
+ `501`.
466
+ responses:
467
+ '200':
468
+ description: Locked.
469
+ content:
470
+ application/json:
471
+ schema:
472
+ $ref: '#/components/schemas/Success'
473
+ example:
474
+ success: true
475
+ '400':
476
+ description: This server can't do this on the device's platform, or the udid is malformed.
477
+ content:
478
+ application/json:
479
+ schema:
480
+ $ref: '#/components/schemas/Error'
481
+ examples:
482
+ invalidUdid:
483
+ summary: The udid segment is not valid percent-encoding
484
+ value:
485
+ success: false
486
+ error: invalid_udid
487
+ notSupported:
488
+ summary: No device manager for the platform, or it can't do this
489
+ value:
490
+ error: not_supported
491
+ message: Manager not found or lock not supported
492
+ '401':
493
+ $ref: '#/components/responses/Unauthorized'
494
+ '403':
495
+ $ref: '#/components/responses/ControlForbiddenMutation'
496
+ '404':
497
+ $ref: '#/components/responses/ControlDeviceNotFound'
498
+ '409':
499
+ $ref: '#/components/responses/ControlHeld'
500
+ '429':
501
+ $ref: '#/components/responses/RateLimited'
502
+ '500':
503
+ description: Locking failed on the device (on iOS, WebDriverAgent refused or didn't answer).
504
+ content:
505
+ application/json:
506
+ schema:
507
+ $ref: '#/components/schemas/Error'
508
+ examples:
509
+ android:
510
+ summary: Android
511
+ value:
512
+ error: ADB is not available
513
+ ios:
514
+ summary: iOS
515
+ value:
516
+ error: 'WDA request failed: socket hang up'
517
+ '501':
518
+ $ref: '#/components/responses/ControlCloudPhone'
519
+ '502':
520
+ $ref: '#/components/responses/ControlNodeUnreachable'
521
+ '503':
522
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
523
+ '504':
524
+ $ref: '#/components/responses/ControlNodeTimeout'
525
+ /api/control/{udid}/unlock:
526
+ post:
527
+ tags:
528
+ - Control
529
+ parameters:
530
+ - $ref: '#/components/parameters/ControlUdid'
531
+ operationId: unlockDevice
532
+ summary: Wake and unlock the screen
533
+ description: >-
534
+ Wakes the device and dismisses a lock screen without a passcode. Android sends WAKEUP (224)
535
+ then MENU (82); iOS unlocks through WebDriverAgent, and a WebDriverAgent failure answers
536
+ `500`. A passcode is never entered.
537
+
538
+
539
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
540
+ the device (their live preview or recording) or runs an Appium session on it; admins are
541
+ exempt, and your own Appium session on it stays controllable.
542
+
543
+
544
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
545
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
546
+ `501`.
547
+ responses:
548
+ '200':
549
+ description: Woken and unlocked.
550
+ content:
551
+ application/json:
552
+ schema:
553
+ $ref: '#/components/schemas/Success'
554
+ example:
555
+ success: true
556
+ '400':
557
+ description: This server can't do this on the device's platform, or the udid is malformed.
558
+ content:
559
+ application/json:
560
+ schema:
561
+ $ref: '#/components/schemas/Error'
562
+ examples:
563
+ invalidUdid:
564
+ summary: The udid segment is not valid percent-encoding
565
+ value:
566
+ success: false
567
+ error: invalid_udid
568
+ notSupported:
569
+ summary: No device manager for the platform, or it can't do this
570
+ value:
571
+ error: not_supported
572
+ message: Manager not found or unlock not supported
573
+ '401':
574
+ $ref: '#/components/responses/Unauthorized'
575
+ '403':
576
+ $ref: '#/components/responses/ControlForbiddenMutation'
577
+ '404':
578
+ $ref: '#/components/responses/ControlDeviceNotFound'
579
+ '409':
580
+ $ref: '#/components/responses/ControlHeld'
581
+ '429':
582
+ $ref: '#/components/responses/RateLimited'
583
+ '500':
584
+ description: Unlocking failed on the device (on iOS, WebDriverAgent refused or didn't answer).
585
+ content:
586
+ application/json:
587
+ schema:
588
+ $ref: '#/components/schemas/Error'
589
+ examples:
590
+ android:
591
+ summary: Android
592
+ value:
593
+ error: ADB is not available
594
+ ios:
595
+ summary: iOS
596
+ value:
597
+ error: 'WDA request failed: socket hang up'
598
+ '501':
599
+ $ref: '#/components/responses/ControlCloudPhone'
600
+ '502':
601
+ $ref: '#/components/responses/ControlNodeUnreachable'
602
+ '503':
603
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
604
+ '504':
605
+ $ref: '#/components/responses/ControlNodeTimeout'
606
+ /api/control/{udid}/screenshot:
607
+ get:
608
+ tags:
609
+ - Control
610
+ parameters:
611
+ - $ref: '#/components/parameters/ControlUdid'
612
+ operationId: getDeviceScreenshot
613
+ summary: Take a screenshot
614
+ description: >-
615
+ Captures the screen as base64. Android reuses the latest frame of a running MJPEG preview
616
+ when there is one (a JPEG), else runs `screencap` (a PNG). iOS uses go-ios (opening the
617
+ phone's tunnel on iOS 17+ if needed), then WebDriverAgent (PNG).
618
+
619
+
620
+ Open to watchers: a busy device can be read by anyone who can see it. Needs the `MEMBER`
621
+ role; any scope will do.
622
+
623
+
624
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
625
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
626
+ `501`.
627
+ responses:
628
+ '200':
629
+ description: The screenshot.
630
+ content:
631
+ application/json:
632
+ schema:
633
+ $ref: '#/components/schemas/ControlScreenshot'
634
+ example:
635
+ screenshot: iVBORw0KGgoAAAANSUhEUgAABkAAAAzQCAYAAAB...
636
+ '400':
637
+ description: This server can't do this on the device's platform, or the udid is malformed.
638
+ content:
639
+ application/json:
640
+ schema:
641
+ $ref: '#/components/schemas/Error'
642
+ examples:
643
+ invalidUdid:
644
+ summary: The udid segment is not valid percent-encoding
645
+ value:
646
+ success: false
647
+ error: invalid_udid
648
+ notSupported:
649
+ summary: No device manager for the platform, or it can't do this
650
+ value:
651
+ error: not_supported
652
+ message: Manager not found or screenshot not supported
653
+ '401':
654
+ $ref: '#/components/responses/Unauthorized'
655
+ '403':
656
+ $ref: '#/components/responses/ControlForbiddenRead'
657
+ '404':
658
+ $ref: '#/components/responses/ControlDeviceNotFound'
659
+ '429':
660
+ $ref: '#/components/responses/RateLimited'
661
+ '501':
662
+ $ref: '#/components/responses/ControlCloudPhone'
663
+ '502':
664
+ description: >-
665
+ The device didn't give a usable image, or, on a hub, the phone's node couldn't be
666
+ reached (`node_unreachable`).
667
+ content:
668
+ application/json:
669
+ schema:
670
+ $ref: '#/components/schemas/Error'
671
+ examples:
672
+ captureFailed:
673
+ summary: No usable image
674
+ value:
675
+ error: >-
676
+ Screenshot capture failed. Device may be busy or WDA is unresponsive. Try
677
+ again.
678
+ nodeUnreachable:
679
+ summary: The node can't be reached
680
+ value:
681
+ success: false
682
+ error: node_unreachable
683
+ message: 'Xenon could not reach node http://10.0.4.21:4723 for this phone: ECONNREFUSED'
684
+ '503':
685
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
686
+ '504':
687
+ $ref: '#/components/responses/ControlNodeTimeout'
688
+ /api/control/{udid}/display:
689
+ get:
690
+ tags:
691
+ - Control
692
+ parameters:
693
+ - $ref: '#/components/parameters/ControlUdid'
694
+ operationId: getDeviceDisplayState
695
+ summary: Tell whether the screen is lit
696
+ description: >-
697
+ Says whether the panel is on, so a black preview can be told apart from a sleeping device.
698
+ Android reads `dumpsys power` (cached for 2 seconds per device); platforms with no reader,
699
+ iOS included, answer `unknown`. Never fails on the device side: an unreadable device is
700
+ `unknown`.
701
+
702
+
703
+ Open to watchers. Needs the `MEMBER` role; any scope will do.
704
+
705
+
706
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
707
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
708
+ `501`.
709
+ responses:
710
+ '200':
711
+ description: The display state.
712
+ content:
713
+ application/json:
714
+ schema:
715
+ $ref: '#/components/schemas/ControlDisplayState'
716
+ example:
717
+ state: 'off'
718
+ '400':
719
+ description: The udid is malformed.
720
+ content:
721
+ application/json:
722
+ schema:
723
+ $ref: '#/components/schemas/Error'
724
+ examples:
725
+ invalidUdid:
726
+ summary: The udid segment is not valid percent-encoding
727
+ value:
728
+ success: false
729
+ error: invalid_udid
730
+ '401':
731
+ $ref: '#/components/responses/Unauthorized'
732
+ '403':
733
+ $ref: '#/components/responses/ControlForbiddenRead'
734
+ '404':
735
+ $ref: '#/components/responses/ControlDeviceNotFound'
736
+ '429':
737
+ $ref: '#/components/responses/RateLimited'
738
+ '501':
739
+ $ref: '#/components/responses/ControlCloudPhone'
740
+ '502':
741
+ $ref: '#/components/responses/ControlNodeUnreachable'
742
+ '503':
743
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
744
+ '504':
745
+ $ref: '#/components/responses/ControlNodeTimeout'
746
+ /api/control/{udid}/clipboard:
747
+ get:
748
+ tags:
749
+ - Control
750
+ parameters:
751
+ - $ref: '#/components/parameters/ControlUdid'
752
+ operationId: getDeviceClipboard
753
+ summary: Read the device's clipboard
754
+ description: >-
755
+ Returns the clipboard's plain text. Android reads it through the Appium Settings app, which
756
+ it makes the input method for a moment and then restores; iOS reads it through
757
+ WebDriverAgent (on a real iPhone WebDriverAgent is brought to the foreground first, as iOS
758
+ requires).
759
+
760
+
761
+ Unlike other reads this one is ownership-checked: while another user holds the device or
762
+ runs a session on it you get `409`, since the clipboard is their work (often a password or
763
+ code). Needs the `MEMBER` role; any scope will do.
764
+
765
+
766
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
767
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
768
+ `501`.
769
+ responses:
770
+ '200':
771
+ description: The clipboard text (empty when the clipboard is empty).
772
+ content:
773
+ application/json:
774
+ schema:
775
+ $ref: '#/components/schemas/ControlClipboard'
776
+ example:
777
+ content: https://example.com/reset?code=4821
778
+ '400':
779
+ description: This server can't do this on the device's platform, or the udid is malformed.
780
+ content:
781
+ application/json:
782
+ schema:
783
+ $ref: '#/components/schemas/Error'
784
+ examples:
785
+ invalidUdid:
786
+ summary: The udid segment is not valid percent-encoding
787
+ value:
788
+ success: false
789
+ error: invalid_udid
790
+ notSupported:
791
+ summary: No device manager for the platform, or it can't do this
792
+ value:
793
+ error: not_supported
794
+ message: Manager not found or getClipboard not supported
795
+ '401':
796
+ $ref: '#/components/responses/Unauthorized'
797
+ '403':
798
+ $ref: '#/components/responses/ControlForbiddenRead'
799
+ '404':
800
+ $ref: '#/components/responses/ControlDeviceNotFound'
801
+ '409':
802
+ $ref: '#/components/responses/ControlHeld'
803
+ '429':
804
+ $ref: '#/components/responses/RateLimited'
805
+ '500':
806
+ description: Reading the clipboard failed on the device.
807
+ content:
808
+ application/json:
809
+ schema:
810
+ $ref: '#/components/schemas/Error'
811
+ example:
812
+ error: Appium Settings is not installed on R5CT32ABCDE
813
+ '501':
814
+ $ref: '#/components/responses/ControlCloudPhone'
815
+ '502':
816
+ $ref: '#/components/responses/ControlNodeUnreachable'
817
+ '503':
818
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
819
+ '504':
820
+ $ref: '#/components/responses/ControlNodeTimeout'
821
+ post:
822
+ tags:
823
+ - Control
824
+ parameters:
825
+ - $ref: '#/components/parameters/ControlUdid'
826
+ operationId: setDeviceClipboard
827
+ summary: Set the device's clipboard
828
+ description: >-
829
+ Puts `content` on the clipboard as plain text. **iOS only:** Android can't set the clipboard
830
+ outside a test session and answers `501`. A WebDriverAgent failure on iOS answers `500`.
831
+
832
+
833
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
834
+ the device (their live preview or recording) or runs an Appium session on it; admins are
835
+ exempt, and your own Appium session on it stays controllable.
836
+
837
+
838
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
839
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
840
+ `501`.
841
+ requestBody:
842
+ required: true
843
+ content:
844
+ application/json:
845
+ schema:
846
+ $ref: '#/components/schemas/ControlClipboard'
847
+ example:
848
+ content: Hunter2-staging
849
+ responses:
850
+ '200':
851
+ description: Set.
852
+ content:
853
+ application/json:
854
+ schema:
855
+ $ref: '#/components/schemas/Success'
856
+ example:
857
+ success: true
858
+ '400':
859
+ description: This server can't do this on the device's platform, or the udid is malformed.
860
+ content:
861
+ application/json:
862
+ schema:
863
+ $ref: '#/components/schemas/Error'
864
+ examples:
865
+ invalidUdid:
866
+ summary: The udid segment is not valid percent-encoding
867
+ value:
868
+ success: false
869
+ error: invalid_udid
870
+ notSupported:
871
+ summary: No device manager for the platform, or it can't do this
872
+ value:
873
+ error: not_supported
874
+ message: Manager not found or setClipboard not supported
875
+ '401':
876
+ $ref: '#/components/responses/Unauthorized'
877
+ '403':
878
+ $ref: '#/components/responses/ControlForbiddenMutation'
879
+ '404':
880
+ $ref: '#/components/responses/ControlDeviceNotFound'
881
+ '409':
882
+ $ref: '#/components/responses/ControlHeld'
883
+ '429':
884
+ $ref: '#/components/responses/RateLimited'
885
+ '500':
886
+ description: Setting the clipboard failed on the device.
887
+ content:
888
+ application/json:
889
+ schema:
890
+ $ref: '#/components/schemas/Error'
891
+ example:
892
+ error: 'WDA request failed: socket hang up'
893
+ '501':
894
+ description: >-
895
+ Android can't set its clipboard from here; or, on a hub, the phone is a cloud provider's
896
+ (`not_available_for_cloud_phone`).
897
+ content:
898
+ application/json:
899
+ schema:
900
+ $ref: '#/components/schemas/Error'
901
+ examples:
902
+ android:
903
+ summary: An Android device
904
+ value:
905
+ error: >-
906
+ Android doesn’t allow setting the clipboard from here: Appium Settings can
907
+ only read it.
908
+ cloud:
909
+ summary: A cloud provider's phone, on a hub
910
+ value:
911
+ success: false
912
+ error: not_available_for_cloud_phone
913
+ message: This phone is a cloud provider's. Device control isn't available for it here.
914
+ '502':
915
+ $ref: '#/components/responses/ControlNodeUnreachable'
916
+ '503':
917
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
918
+ '504':
919
+ $ref: '#/components/responses/ControlNodeTimeout'
920
+ /api/control/{udid}/apps:
921
+ get:
922
+ tags:
923
+ - Control
924
+ parameters:
925
+ - $ref: '#/components/parameters/ControlUdid'
926
+ operationId: listDeviceApps
927
+ summary: List the apps installed on the device
928
+ description: >-
929
+ Returns the installed apps' package names (Android: third-party packages from `pm list
930
+ packages -3`) or bundle ids (iOS: `ideviceinstaller`, falling back to go-ios).
931
+
932
+
933
+ Open to watchers. Needs the `MEMBER` role; any scope will do.
934
+
935
+
936
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
937
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
938
+ `501`.
939
+ responses:
940
+ '200':
941
+ description: Package names or bundle ids.
942
+ content:
943
+ application/json:
944
+ schema:
945
+ type: array
946
+ items:
947
+ type: string
948
+ example:
949
+ - com.example.shop
950
+ - com.example.shop.debug
951
+ - io.appium.settings
952
+ '400':
953
+ description: This server can't do this on the device's platform, or the udid is malformed.
954
+ content:
955
+ application/json:
956
+ schema:
957
+ $ref: '#/components/schemas/Error'
958
+ examples:
959
+ invalidUdid:
960
+ summary: The udid segment is not valid percent-encoding
961
+ value:
962
+ success: false
963
+ error: invalid_udid
964
+ notSupported:
965
+ summary: No device manager for the platform, or it can't do this
966
+ value:
967
+ error: not_supported
968
+ message: Manager not found or listApps not supported
969
+ '401':
970
+ $ref: '#/components/responses/Unauthorized'
971
+ '403':
972
+ $ref: '#/components/responses/ControlForbiddenRead'
973
+ '404':
974
+ $ref: '#/components/responses/ControlDeviceNotFound'
975
+ '429':
976
+ $ref: '#/components/responses/RateLimited'
977
+ '500':
978
+ description: Listing apps failed on the device.
979
+ content:
980
+ application/json:
981
+ schema:
982
+ $ref: '#/components/schemas/Error'
983
+ example:
984
+ error: 'Command failed: ideviceinstaller -u 00008110-00084CE80E51401E list'
985
+ '501':
986
+ $ref: '#/components/responses/ControlCloudPhone'
987
+ '502':
988
+ $ref: '#/components/responses/ControlNodeUnreachable'
989
+ '503':
990
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
991
+ '504':
992
+ $ref: '#/components/responses/ControlNodeTimeout'
993
+ /api/control/{udid}/install:
994
+ post:
995
+ tags:
996
+ - Control
997
+ parameters:
998
+ - $ref: '#/components/parameters/ControlUdid'
999
+ operationId: installAppFromPath
1000
+ summary: Install an app from a path on the server
1001
+ description: >-
1002
+ Installs the `.apk`, `.ipa` or `.app` at `appPath`, a path on **this server's** file system
1003
+ (Android installs with `adb install -r`, replacing an existing version). To send a file from
1004
+ your machine use `upload-install`; to install from the app library use
1005
+ `install-repository-app`.
1006
+
1007
+
1008
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
1009
+ the device (their live preview or recording) or runs an Appium session on it; admins are
1010
+ exempt, and your own Appium session on it stays controllable.
1011
+
1012
+
1013
+ On a hub this isn't available for a node's phone (a path names a file on one machine): it
1014
+ answers `501 not_available_through_hub`. A cloud provider's phone answers `501` too.
1015
+ requestBody:
1016
+ required: true
1017
+ content:
1018
+ application/json:
1019
+ schema:
1020
+ $ref: '#/components/schemas/ControlInstallPathRequest'
1021
+ example:
1022
+ appPath: /opt/builds/shop-2.14.0-debug.apk
1023
+ responses:
1024
+ '200':
1025
+ description: Installed.
1026
+ content:
1027
+ application/json:
1028
+ schema:
1029
+ $ref: '#/components/schemas/Success'
1030
+ example:
1031
+ success: true
1032
+ '400':
1033
+ description: This server can't do this on the device's platform, or the udid is malformed.
1034
+ content:
1035
+ application/json:
1036
+ schema:
1037
+ $ref: '#/components/schemas/Error'
1038
+ examples:
1039
+ invalidUdid:
1040
+ summary: The udid segment is not valid percent-encoding
1041
+ value:
1042
+ success: false
1043
+ error: invalid_udid
1044
+ notSupported:
1045
+ summary: No device manager for the platform, or it can't do this
1046
+ value:
1047
+ error: not_supported
1048
+ message: Manager not found or installApp not supported
1049
+ '401':
1050
+ $ref: '#/components/responses/Unauthorized'
1051
+ '403':
1052
+ $ref: '#/components/responses/ControlForbiddenMutation'
1053
+ '404':
1054
+ $ref: '#/components/responses/ControlDeviceNotFound'
1055
+ '409':
1056
+ $ref: '#/components/responses/ControlHeld'
1057
+ '429':
1058
+ $ref: '#/components/responses/RateLimited'
1059
+ '500':
1060
+ description: The install failed on the device.
1061
+ content:
1062
+ application/json:
1063
+ schema:
1064
+ $ref: '#/components/schemas/Error'
1065
+ example:
1066
+ error: 'Command failed: adb -s R5CT32ABCDE install -r /opt/builds/shop-2.14.0-debug.apk'
1067
+ '501':
1068
+ description: >-
1069
+ On a hub: the phone is a node's, and the hub doesn't pass this action on, or it is a
1070
+ cloud provider's.
1071
+ content:
1072
+ application/json:
1073
+ schema:
1074
+ $ref: '#/components/schemas/Error'
1075
+ examples:
1076
+ node:
1077
+ summary: A node's phone
1078
+ value:
1079
+ success: false
1080
+ error: not_available_through_hub
1081
+ message: >-
1082
+ This phone is on node http://10.0.4.21:4723, and the hub doesn't pass install
1083
+ on to nodes yet.
1084
+ cloud:
1085
+ summary: A cloud provider's phone
1086
+ value:
1087
+ success: false
1088
+ error: not_available_for_cloud_phone
1089
+ message: This phone is a cloud provider's. Device control isn't available for it here.
1090
+ '503':
1091
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1092
+ /api/control/{udid}/install-repository-app:
1093
+ post:
1094
+ tags:
1095
+ - Control
1096
+ parameters:
1097
+ - $ref: '#/components/parameters/ControlUdid'
1098
+ operationId: installLibraryApp
1099
+ summary: Install an app from the app library
1100
+ description: >-
1101
+ Installs an uploaded app (see Applications) by its id. The app must be visible to you:
1102
+ another team's app answers `404` exactly like an unknown one. The answer comes when the
1103
+ install has finished.
1104
+
1105
+
1106
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
1107
+ the device (their live preview or recording) or runs an Appium session on it; admins are
1108
+ exempt, and your own Appium session on it stays controllable.
1109
+
1110
+
1111
+ On a hub, for a node's phone, the app file (which only the hub has) is streamed to the
1112
+ node's `upload-install` and the node's answer is relayed; the node has up to 15 minutes to
1113
+ start answering. A cloud provider's phone answers `501`.
1114
+ requestBody:
1115
+ required: true
1116
+ content:
1117
+ application/json:
1118
+ schema:
1119
+ $ref: '#/components/schemas/ControlInstallLibraryAppRequest'
1120
+ example:
1121
+ appId: b3c1f0e2-6a8d-4b6e-9a51-2f7d0c9e4a13
1122
+ responses:
1123
+ '200':
1124
+ description: Installed.
1125
+ content:
1126
+ application/json:
1127
+ schema:
1128
+ $ref: '#/components/schemas/ControlInstallResult'
1129
+ example:
1130
+ success: true
1131
+ message: Installed Shop
1132
+ '400':
1133
+ description: >-
1134
+ `appId` is missing, empty or not a string, this server can't do this on the device's
1135
+ platform, or the udid is malformed.
1136
+ content:
1137
+ application/json:
1138
+ schema:
1139
+ $ref: '#/components/schemas/Error'
1140
+ examples:
1141
+ invalidUdid:
1142
+ summary: The udid segment is not valid percent-encoding
1143
+ value:
1144
+ success: false
1145
+ error: invalid_udid
1146
+ noAppId:
1147
+ summary: No `appId`
1148
+ value:
1149
+ error: bad_request
1150
+ message: appId is required
1151
+ notSupported:
1152
+ summary: No device manager for the platform, or it can't do this
1153
+ value:
1154
+ error: not_supported
1155
+ message: Manager not found or installApp not supported
1156
+ '401':
1157
+ $ref: '#/components/responses/Unauthorized'
1158
+ '403':
1159
+ $ref: '#/components/responses/ControlForbiddenMutation'
1160
+ '404':
1161
+ description: No such device or app, or one of them is outside your teams (each answers as unknown).
1162
+ content:
1163
+ application/json:
1164
+ schema:
1165
+ $ref: '#/components/schemas/Error'
1166
+ examples:
1167
+ device:
1168
+ summary: Unknown or hidden device
1169
+ value:
1170
+ error: not_found
1171
+ message: Device not found
1172
+ app:
1173
+ summary: Unknown or hidden app
1174
+ value:
1175
+ error: not_found
1176
+ message: App not found in repository
1177
+ '409':
1178
+ $ref: '#/components/responses/ControlHeld'
1179
+ '429':
1180
+ $ref: '#/components/responses/RateLimited'
1181
+ '500':
1182
+ description: The install failed on the device.
1183
+ content:
1184
+ application/json:
1185
+ schema:
1186
+ $ref: '#/components/schemas/Error'
1187
+ example:
1188
+ error: >-
1189
+ Command failed: adb -s R5CT32ABCDE install -r
1190
+ /home/xenon/.cache/xenon/apps/shop-2.14.0.apk
1191
+ '501':
1192
+ $ref: '#/components/responses/ControlCloudPhone'
1193
+ '502':
1194
+ $ref: '#/components/responses/ControlNodeUnreachable'
1195
+ '503':
1196
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1197
+ '504':
1198
+ $ref: '#/components/responses/ControlNodeTimeout'
1199
+ /api/control/{udid}/upload-install:
1200
+ post:
1201
+ tags:
1202
+ - Control
1203
+ parameters:
1204
+ - $ref: '#/components/parameters/ControlUdid'
1205
+ operationId: uploadAndInstallApp
1206
+ summary: Upload an app file and install it
1207
+ description: >-
1208
+ Uploads an `.apk`, `.ipa` or zipped `.app` as the multipart field `app` (at most 4 GiB) and
1209
+ installs it. The file is written to a temporary file, installed, and deleted before the
1210
+ answer is sent, whether the install worked or not. The answer comes when the install has
1211
+ finished.
1212
+
1213
+
1214
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
1215
+ the device (their live preview or recording) or runs an Appium session on it; admins are
1216
+ exempt, and your own Appium session on it stays controllable.
1217
+
1218
+
1219
+ On a hub, for a node's phone, the upload is streamed on to the node as it arrives, and the
1220
+ node has up to 15 minutes to start answering. A cloud provider's phone answers `501`.
1221
+ requestBody:
1222
+ required: true
1223
+ content:
1224
+ multipart/form-data:
1225
+ schema:
1226
+ $ref: '#/components/schemas/ControlUploadInstallRequest'
1227
+ responses:
1228
+ '200':
1229
+ description: Installed.
1230
+ content:
1231
+ application/json:
1232
+ schema:
1233
+ $ref: '#/components/schemas/ControlInstallResult'
1234
+ example:
1235
+ success: true
1236
+ message: App shop-2.14.0-debug.apk installed successfully
1237
+ '400':
1238
+ description: No file, no `app` field, no installer for this platform, or a malformed udid.
1239
+ content:
1240
+ application/json:
1241
+ schema:
1242
+ $ref: '#/components/schemas/Error'
1243
+ examples:
1244
+ invalidUdid:
1245
+ summary: The udid segment is not valid percent-encoding
1246
+ value:
1247
+ success: false
1248
+ error: invalid_udid
1249
+ noFiles:
1250
+ summary: No file in the request
1251
+ value:
1252
+ error: bad_request
1253
+ message: No files were uploaded.
1254
+ noAppField:
1255
+ summary: A file, but not in the `app` field
1256
+ value:
1257
+ error: bad_request
1258
+ message: File "app" is required
1259
+ notSupported:
1260
+ summary: No installer for this platform
1261
+ value:
1262
+ error: not_supported
1263
+ message: Manager not found or installApp not supported
1264
+ '401':
1265
+ $ref: '#/components/responses/Unauthorized'
1266
+ '403':
1267
+ $ref: '#/components/responses/ControlForbiddenMutation'
1268
+ '404':
1269
+ $ref: '#/components/responses/ControlDeviceNotFound'
1270
+ '409':
1271
+ $ref: '#/components/responses/ControlHeld'
1272
+ '413':
1273
+ description: The file is larger than 4 GiB.
1274
+ content:
1275
+ text/html:
1276
+ schema:
1277
+ type: string
1278
+ example: File size limit has been reached
1279
+ '429':
1280
+ $ref: '#/components/responses/RateLimited'
1281
+ '500':
1282
+ description: The install failed on the device.
1283
+ content:
1284
+ application/json:
1285
+ schema:
1286
+ $ref: '#/components/schemas/Error'
1287
+ example:
1288
+ error: >-
1289
+ Command failed: adb -s R5CT32ABCDE install -r
1290
+ /tmp/xenon-uploads/1759571234567-shop-2.14.0-debug.apk
1291
+ '501':
1292
+ $ref: '#/components/responses/ControlCloudPhone'
1293
+ '502':
1294
+ $ref: '#/components/responses/ControlNodeUnreachable'
1295
+ '503':
1296
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1297
+ '504':
1298
+ $ref: '#/components/responses/ControlNodeTimeout'
1299
+ /api/control/{udid}/uninstall:
1300
+ post:
1301
+ tags:
1302
+ - Control
1303
+ parameters:
1304
+ - $ref: '#/components/parameters/ControlUdid'
1305
+ operationId: uninstallDeviceApp
1306
+ summary: Uninstall an app
1307
+ description: >-
1308
+ Removes the app with package name or bundle id `bundleId` from the device.
1309
+
1310
+
1311
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
1312
+ the device (their live preview or recording) or runs an Appium session on it; admins are
1313
+ exempt, and your own Appium session on it stays controllable.
1314
+
1315
+
1316
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
1317
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
1318
+ `501`.
1319
+ requestBody:
1320
+ required: true
1321
+ content:
1322
+ application/json:
1323
+ schema:
1324
+ $ref: '#/components/schemas/ControlUninstallRequest'
1325
+ example:
1326
+ bundleId: com.example.shop
1327
+ responses:
1328
+ '200':
1329
+ description: Uninstalled.
1330
+ content:
1331
+ application/json:
1332
+ schema:
1333
+ $ref: '#/components/schemas/Success'
1334
+ example:
1335
+ success: true
1336
+ '400':
1337
+ description: This server can't do this on the device's platform, or the udid is malformed.
1338
+ content:
1339
+ application/json:
1340
+ schema:
1341
+ $ref: '#/components/schemas/Error'
1342
+ examples:
1343
+ invalidUdid:
1344
+ summary: The udid segment is not valid percent-encoding
1345
+ value:
1346
+ success: false
1347
+ error: invalid_udid
1348
+ notSupported:
1349
+ summary: No device manager for the platform, or it can't do this
1350
+ value:
1351
+ error: not_supported
1352
+ message: Manager not found or uninstallApp not supported
1353
+ '401':
1354
+ $ref: '#/components/responses/Unauthorized'
1355
+ '403':
1356
+ $ref: '#/components/responses/ControlForbiddenMutation'
1357
+ '404':
1358
+ $ref: '#/components/responses/ControlDeviceNotFound'
1359
+ '409':
1360
+ $ref: '#/components/responses/ControlHeld'
1361
+ '429':
1362
+ $ref: '#/components/responses/RateLimited'
1363
+ '500':
1364
+ description: The uninstall failed on the device.
1365
+ content:
1366
+ application/json:
1367
+ schema:
1368
+ $ref: '#/components/schemas/Error'
1369
+ example:
1370
+ error: 'Command failed: adb -s R5CT32ABCDE uninstall com.example.shop'
1371
+ '501':
1372
+ $ref: '#/components/responses/ControlCloudPhone'
1373
+ '502':
1374
+ $ref: '#/components/responses/ControlNodeUnreachable'
1375
+ '503':
1376
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1377
+ '504':
1378
+ $ref: '#/components/responses/ControlNodeTimeout'
1379
+ /api/control/{udid}/logs:
1380
+ get:
1381
+ tags:
1382
+ - Control
1383
+ parameters:
1384
+ - $ref: '#/components/parameters/ControlUdid'
1385
+ operationId: getDeviceLogs
1386
+ summary: Read recent device logs
1387
+ description: >-
1388
+ Returns recent device log lines as one string.
1389
+
1390
+ - **Android:** the last 500 lines of `logcat` (`-v threadtime`). If `adb` fails or isn't
1391
+ available, the answer is `500` with the reason in `error`.
1392
+
1393
+ - **iOS (real device):** the lines the syslog stream has gathered since the previous call.
1394
+ The first call starts that stream and may return nothing; each call empties the buffer, so
1395
+ two clients polling the same phone split the lines between them. The stream stops after a
1396
+ while without calls.
1397
+
1398
+ - **iOS simulator:** the last 10 seconds of `log show`.
1399
+
1400
+ For a continuous Android log use the logcat WebSocket (see `POST
1401
+ /api/control/{udid}/stream/ticket`).
1402
+
1403
+ Ownership-checked like the clipboard: logs carry tokens and personal data from the app under
1404
+ test, so while another user holds the device or runs a session on it you get `409`. Needs
1405
+ the `MEMBER` role; any scope will do.
1406
+
1407
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
1408
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
1409
+ `501`.
1410
+ responses:
1411
+ '200':
1412
+ description: The log text.
1413
+ content:
1414
+ application/json:
1415
+ schema:
1416
+ $ref: '#/components/schemas/ControlLogs'
1417
+ example:
1418
+ logs: >-
1419
+ 10-04 14:02:11.532 1203 1250 I ActivityManager: Start proc
1420
+ 8812:com.example.shop/u0a201 for activity
1421
+
1422
+ 10-04 14:02:11.871 8812 8812 D ShopApp: onCreate
1423
+ '400':
1424
+ description: This server can't do this on the device's platform, or the udid is malformed.
1425
+ content:
1426
+ application/json:
1427
+ schema:
1428
+ $ref: '#/components/schemas/Error'
1429
+ examples:
1430
+ invalidUdid:
1431
+ summary: The udid segment is not valid percent-encoding
1432
+ value:
1433
+ success: false
1434
+ error: invalid_udid
1435
+ notSupported:
1436
+ summary: No device manager for the platform, or it can't do this
1437
+ value:
1438
+ error: not_supported
1439
+ message: Manager not found or getLogs not supported
1440
+ '401':
1441
+ $ref: '#/components/responses/Unauthorized'
1442
+ '403':
1443
+ $ref: '#/components/responses/ControlForbiddenRead'
1444
+ '404':
1445
+ $ref: '#/components/responses/ControlDeviceNotFound'
1446
+ '409':
1447
+ $ref: '#/components/responses/ControlHeld'
1448
+ '429':
1449
+ $ref: '#/components/responses/RateLimited'
1450
+ '500':
1451
+ description: Reading logs failed on the device (on Android, `adb` failed or isn't available).
1452
+ content:
1453
+ application/json:
1454
+ schema:
1455
+ $ref: '#/components/schemas/Error'
1456
+ examples:
1457
+ android:
1458
+ summary: Android, no adb
1459
+ value:
1460
+ error: ADB is not available
1461
+ ios:
1462
+ summary: iOS
1463
+ value:
1464
+ error: spawn idevicesyslog ENOENT
1465
+ '501':
1466
+ $ref: '#/components/responses/ControlCloudPhone'
1467
+ '502':
1468
+ $ref: '#/components/responses/ControlNodeUnreachable'
1469
+ '503':
1470
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1471
+ '504':
1472
+ $ref: '#/components/responses/ControlNodeTimeout'
1473
+ /api/control/{udid}/shell:
1474
+ post:
1475
+ tags:
1476
+ - Control
1477
+ parameters:
1478
+ - $ref: '#/components/parameters/ControlUdid'
1479
+ operationId: runDeviceShellCommand
1480
+ summary: Run an allowed shell command
1481
+ description: >-
1482
+ Runs one diagnostic command from an allowlist and returns its output. Anything else is
1483
+ refused, and so is a command that fails, **with `200`** and an `error` field instead of
1484
+ `output` (the dashboard's terminal prints either).
1485
+
1486
+ - **Android** (`adb shell`): commands starting with `ls`, `ps`, `top`, `dumpsys battery`,
1487
+ `dumpsys wifi`, `dumpsys power`, `whoami`, `getprop`, `pm list packages`, `ip addr`, `cat
1488
+ /proc/meminfo`, `cat /proc/cpuinfo`, `date`, `uptime` or `netstat`.
1489
+
1490
+ - **iOS:** simulator commands `listapps`, `get_app_container`, `list`, `getenv` (through
1491
+ `simctl`); go-ios commands `apps`, `info`, `syslog`, `list`, `deviceinfo`, `diagnostics`;
1492
+ and `ls`, `ps`, `top`, `whoami`, `date`, `uptime`, `netstat`, `id`.
1493
+
1494
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
1495
+ the device (their live preview or recording) or runs an Appium session on it; admins are
1496
+ exempt, and your own Appium session on it stays controllable.
1497
+
1498
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
1499
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
1500
+ `501`.
1501
+ requestBody:
1502
+ required: true
1503
+ content:
1504
+ application/json:
1505
+ schema:
1506
+ $ref: '#/components/schemas/ControlShellRequest'
1507
+ example:
1508
+ command: dumpsys battery
1509
+ responses:
1510
+ '200':
1511
+ description: The output, or an `error` for a refused or failed command.
1512
+ content:
1513
+ application/json:
1514
+ schema:
1515
+ $ref: '#/components/schemas/ControlShellResult'
1516
+ examples:
1517
+ output:
1518
+ summary: The command ran
1519
+ value:
1520
+ output: |
1521
+ Current Battery Service state:
1522
+ AC powered: false
1523
+ USB powered: true
1524
+ level: 87
1525
+ refused:
1526
+ summary: Not on the allowlist
1527
+ value:
1528
+ error: Command 'rm -rf /sdcard' is not allowed for security reasons.
1529
+ '400':
1530
+ description: '`command` is missing or empty, the platform has no shell here, or the udid is malformed.'
1531
+ content:
1532
+ application/json:
1533
+ schema:
1534
+ $ref: '#/components/schemas/Error'
1535
+ examples:
1536
+ invalidUdid:
1537
+ summary: The udid segment is not valid percent-encoding
1538
+ value:
1539
+ success: false
1540
+ error: invalid_udid
1541
+ noCommand:
1542
+ summary: No `command`
1543
+ value:
1544
+ error: bad_request
1545
+ message: Command is required
1546
+ notSupported:
1547
+ summary: No shell for the platform here
1548
+ value:
1549
+ error: not_supported
1550
+ message: Manager not found or executeShell not supported
1551
+ '401':
1552
+ $ref: '#/components/responses/Unauthorized'
1553
+ '403':
1554
+ $ref: '#/components/responses/ControlForbiddenMutation'
1555
+ '404':
1556
+ $ref: '#/components/responses/ControlDeviceNotFound'
1557
+ '409':
1558
+ $ref: '#/components/responses/ControlHeld'
1559
+ '429':
1560
+ $ref: '#/components/responses/RateLimited'
1561
+ '501':
1562
+ $ref: '#/components/responses/ControlCloudPhone'
1563
+ '502':
1564
+ $ref: '#/components/responses/ControlNodeUnreachable'
1565
+ '503':
1566
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1567
+ '504':
1568
+ $ref: '#/components/responses/ControlNodeTimeout'
1569
+ /api/control/{udid}/inspector/snapshot:
1570
+ get:
1571
+ tags:
1572
+ - Control
1573
+ parameters:
1574
+ - $ref: '#/components/parameters/ControlUdid'
1575
+ operationId: getInspectorSnapshot
1576
+ summary: Capture the screen and its element tree
1577
+ description: >-
1578
+ Takes a screenshot and the UI hierarchy together and returns the tree with suggested
1579
+ locators and code for each element. When an Appium session is running on the device the
1580
+ hierarchy is the session's own page source (`hierarchySource: appium-session`); otherwise it
1581
+ is read from the device directly. Missing screen dimensions are filled in and saved.
1582
+
1583
+
1584
+ Open to watchers. Needs the `MEMBER` role; any scope will do.
1585
+
1586
+
1587
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
1588
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
1589
+ `501`.
1590
+ responses:
1591
+ '200':
1592
+ description: The snapshot.
1593
+ content:
1594
+ application/json:
1595
+ schema:
1596
+ $ref: '#/components/schemas/ControlInspectorSnapshot'
1597
+ '400':
1598
+ description: The udid is malformed.
1599
+ content:
1600
+ application/json:
1601
+ schema:
1602
+ $ref: '#/components/schemas/Error'
1603
+ examples:
1604
+ invalidUdid:
1605
+ summary: The udid segment is not valid percent-encoding
1606
+ value:
1607
+ success: false
1608
+ error: invalid_udid
1609
+ '401':
1610
+ $ref: '#/components/responses/Unauthorized'
1611
+ '403':
1612
+ $ref: '#/components/responses/ControlForbiddenRead'
1613
+ '404':
1614
+ $ref: '#/components/responses/ControlDeviceNotFound'
1615
+ '429':
1616
+ $ref: '#/components/responses/RateLimited'
1617
+ '500':
1618
+ description: The snapshot failed on the device.
1619
+ content:
1620
+ application/json:
1621
+ schema:
1622
+ $ref: '#/components/schemas/Error'
1623
+ example:
1624
+ error: Screenshot not supported
1625
+ '501':
1626
+ $ref: '#/components/responses/ControlCloudPhone'
1627
+ '502':
1628
+ $ref: '#/components/responses/ControlNodeUnreachable'
1629
+ '503':
1630
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1631
+ '504':
1632
+ $ref: '#/components/responses/ControlNodeTimeout'
1633
+ /api/control/{udid}/omni-scan:
1634
+ get:
1635
+ tags:
1636
+ - Control
1637
+ parameters:
1638
+ - $ref: '#/components/parameters/ControlUdid'
1639
+ operationId: scanDeviceScreen
1640
+ summary: Analyse the screen with OCR and AI
1641
+ description: >-
1642
+ Screenshots the device, runs OCR over it (every word with its confidence and box) and asks
1643
+ the configured AI provider for a qualitative read of the screen. No Appium session is
1644
+ needed. Slow: allow tens of seconds.
1645
+
1646
+
1647
+ If the analysis itself fails (an empty screenshot, OCR failing), the answer is `500` with
1648
+ `{ status: 'error', message }`. If only the AI step fails, the answer is `200` and
1649
+ `ai_insights` is `null`.
1650
+
1651
+
1652
+ Open to watchers. Needs the `MEMBER` role; any scope will do.
1653
+
1654
+
1655
+ On a hub, for a node's phone, this runs here and asks the node only for what it needs. A
1656
+ cloud provider's phone answers `501`. The OCR and AI run on the hub, with the hub's AI
1657
+ settings, on a screenshot taken by the node.
1658
+ responses:
1659
+ '200':
1660
+ description: The analysis.
1661
+ content:
1662
+ application/json:
1663
+ schema:
1664
+ $ref: '#/components/schemas/ControlOmniScanResult'
1665
+ examples:
1666
+ analysed:
1667
+ summary: Analysed
1668
+ value:
1669
+ status: success
1670
+ value:
1671
+ timestamp: '2026-10-04T14:05:22.118Z'
1672
+ ocr:
1673
+ text: |-
1674
+ Sign in
1675
+ Email
1676
+ Password
1677
+ Forgot password?
1678
+ words:
1679
+ - text: Sign
1680
+ confidence: 96.2
1681
+ bbox:
1682
+ x0: 142
1683
+ y0: 210
1684
+ x1: 188
1685
+ y1: 236
1686
+ ai_insights:
1687
+ summary: >-
1688
+ A sign-in form with email and password fields and a disabled Sign in
1689
+ button.
1690
+ '400':
1691
+ description: This server has no device manager for the platform, or the udid is malformed.
1692
+ content:
1693
+ application/json:
1694
+ schema:
1695
+ $ref: '#/components/schemas/Error'
1696
+ examples:
1697
+ invalidUdid:
1698
+ summary: The udid segment is not valid percent-encoding
1699
+ value:
1700
+ success: false
1701
+ error: invalid_udid
1702
+ notSupported:
1703
+ summary: No device manager for the platform, or it can't do this
1704
+ value:
1705
+ error: not_supported
1706
+ message: Manager not found
1707
+ '401':
1708
+ $ref: '#/components/responses/Unauthorized'
1709
+ '403':
1710
+ $ref: '#/components/responses/ControlForbiddenRead'
1711
+ '404':
1712
+ $ref: '#/components/responses/ControlDeviceNotFound'
1713
+ '429':
1714
+ $ref: '#/components/responses/RateLimited'
1715
+ '500':
1716
+ description: The analysis failed (an empty screenshot, OCR failing).
1717
+ content:
1718
+ application/json:
1719
+ schema:
1720
+ $ref: '#/components/schemas/ControlStatusError'
1721
+ examples:
1722
+ emptyScreenshot:
1723
+ summary: The screenshot was empty
1724
+ value:
1725
+ status: error
1726
+ message: Screenshot capture returned empty data.
1727
+ ocr:
1728
+ summary: OCR failed
1729
+ value:
1730
+ status: error
1731
+ message: OCR worker failed to start
1732
+ '501':
1733
+ $ref: '#/components/responses/ControlCloudPhone'
1734
+ '503':
1735
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1736
+ /api/control/{udid}/test-locator:
1737
+ post:
1738
+ tags:
1739
+ - Control
1740
+ parameters:
1741
+ - $ref: '#/components/parameters/ControlUdid'
1742
+ operationId: testDeviceAiLocator
1743
+ summary: Try an AI locator on the current screen
1744
+ description: >-
1745
+ Runs one of Xenon's AI locator strategies against a fresh screenshot, as a test would with
1746
+ `findElement`, without an Appium session.
1747
+
1748
+ - `-custom:ai-text`: OCR; every word containing `selector` (case-insensitive, confidence
1749
+ above 60%).
1750
+
1751
+ - `-custom:ai-icon`: the configured AI provider looks for what `selector` describes; at most
1752
+ one match, a 40×40 box around the point it names.
1753
+
1754
+ Each match gets a virtual element id (`omni_…`). No match answers `200` with an empty
1755
+ `value`. A failed screenshot, a failed OCR for `ai-text`, or for `ai-icon` no AI provider
1756
+ configured or a failed AI call answers `500`, so a failure never reads as "nothing found".
1757
+ Counts against the `heavy` rate-limit budget.
1758
+
1759
+ Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
1760
+ the device (their live preview or recording) or runs an Appium session on it; admins are
1761
+ exempt, and your own Appium session on it stays controllable.
1762
+
1763
+ On a hub, for a node's phone, this runs here and asks the node only for what it needs. A
1764
+ cloud provider's phone answers `501`. The OCR and AI run on the hub, with the hub's AI
1765
+ settings, on a screenshot taken by the node.
1766
+ requestBody:
1767
+ required: true
1768
+ content:
1769
+ application/json:
1770
+ schema:
1771
+ $ref: '#/components/schemas/ControlTestLocatorRequest'
1772
+ example:
1773
+ strategy: '-custom:ai-text'
1774
+ selector: Sign in
1775
+ responses:
1776
+ '200':
1777
+ description: The matches, possibly none.
1778
+ content:
1779
+ application/json:
1780
+ schema:
1781
+ $ref: '#/components/schemas/ControlTestLocatorResult'
1782
+ example:
1783
+ status: success
1784
+ value:
1785
+ - id: omni_ocr_1759586722118_k3j9x
1786
+ text: Sign
1787
+ rect:
1788
+ x: 142
1789
+ 'y': 210
1790
+ width: 46
1791
+ height: 26
1792
+ confidence: 0.962
1793
+ '400':
1794
+ description: An unsupported `strategy`, no device manager for the platform, or a malformed udid.
1795
+ content:
1796
+ application/json:
1797
+ schema:
1798
+ $ref: '#/components/schemas/Error'
1799
+ examples:
1800
+ invalidUdid:
1801
+ summary: The udid segment is not valid percent-encoding
1802
+ value:
1803
+ success: false
1804
+ error: invalid_udid
1805
+ unsupportedStrategy:
1806
+ summary: Unsupported strategy
1807
+ value:
1808
+ status: error
1809
+ message: 'Unsupported strategy: xpath'
1810
+ notSupported:
1811
+ summary: No device manager for the platform, or it can't do this
1812
+ value:
1813
+ error: not_supported
1814
+ message: Manager not found
1815
+ '401':
1816
+ $ref: '#/components/responses/Unauthorized'
1817
+ '403':
1818
+ $ref: '#/components/responses/ControlForbiddenMutation'
1819
+ '404':
1820
+ $ref: '#/components/responses/ControlDeviceNotFound'
1821
+ '409':
1822
+ $ref: '#/components/responses/ControlHeld'
1823
+ '429':
1824
+ $ref: '#/components/responses/RateLimited'
1825
+ '500':
1826
+ description: >-
1827
+ The screenshot failed, the OCR failed (`ai-text`), or no AI provider is configured or
1828
+ the AI call failed (`ai-icon`).
1829
+ content:
1830
+ application/json:
1831
+ schema:
1832
+ $ref: '#/components/schemas/ControlStatusError'
1833
+ examples:
1834
+ ocr:
1835
+ summary: OCR failed (ai-text)
1836
+ value:
1837
+ status: error
1838
+ message: OCR worker failed to start
1839
+ noProvider:
1840
+ summary: No AI provider configured (ai-icon)
1841
+ value:
1842
+ status: error
1843
+ message: No AI provider is configured
1844
+ '501':
1845
+ $ref: '#/components/responses/ControlCloudPhone'
1846
+ '503':
1847
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1848
+ /api/control/{udid}/appium-session:
1849
+ get:
1850
+ tags:
1851
+ - Control
1852
+ parameters:
1853
+ - $ref: '#/components/parameters/ControlUdid'
1854
+ operationId: getDeviceAppiumSession
1855
+ summary: Get the Appium session running on the device
1856
+ description: >-
1857
+ Returns the id of the Appium session driving the device, if any, and the WebDriver base path
1858
+ this server was started with, so a client can call that session's commands. `sessionId` is
1859
+ `null` when no session runs; a live preview or recording hold is not a session.
1860
+
1861
+
1862
+ Open to watchers. Needs the `MEMBER` role; any scope will do.
1863
+
1864
+
1865
+ On a hub this is answered by the hub for every phone, a node's or a cloud provider's
1866
+ included, since the session's commands go through the hub.
1867
+ responses:
1868
+ '200':
1869
+ description: The session, or none.
1870
+ content:
1871
+ application/json:
1872
+ schema:
1873
+ $ref: '#/components/schemas/ControlAppiumSession'
1874
+ examples:
1875
+ running:
1876
+ summary: A session is running
1877
+ value:
1878
+ status: success
1879
+ sessionId: 3f9c2a7e-1b4d-4c8a-9e2f-6d5b8a1c0e47
1880
+ basePath: /wd/hub
1881
+ none:
1882
+ summary: Nothing is driving the device
1883
+ value:
1884
+ status: success
1885
+ sessionId: null
1886
+ basePath: ''
1887
+ '400':
1888
+ description: The udid is malformed.
1889
+ content:
1890
+ application/json:
1891
+ schema:
1892
+ $ref: '#/components/schemas/Error'
1893
+ examples:
1894
+ invalidUdid:
1895
+ summary: The udid segment is not valid percent-encoding
1896
+ value:
1897
+ success: false
1898
+ error: invalid_udid
1899
+ '401':
1900
+ $ref: '#/components/responses/Unauthorized'
1901
+ '403':
1902
+ $ref: '#/components/responses/ControlForbiddenRead'
1903
+ '404':
1904
+ $ref: '#/components/responses/ControlDeviceNotFound'
1905
+ '429':
1906
+ $ref: '#/components/responses/RateLimited'
1907
+ '503':
1908
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
1909
+ /api/control/{udid}/stream/start:
1910
+ post:
1911
+ tags:
1912
+ - Control
1913
+ parameters:
1914
+ - $ref: '#/components/parameters/ControlUdid'
1915
+ operationId: startDevicePreview
1916
+ summary: Start the live preview and hold the device
1917
+ description: >-
1918
+ Starts the device's live preview and holds the device for you (`session_id` becomes
1919
+ `manual_<yourUserId>_<udid>`), so automation is not given it while you watch. iOS starts
1920
+ WebDriverAgent's MJPEG stream (and the go-ios tunnel on iOS 17+). Android starts an MJPEG
1921
+ screen capture, or, when the server's `streaming.androidH264` setting is on and the device
1922
+ is not being recorded, an H.264 stream for the WebSocket in `h264Path`. Missing screen
1923
+ dimensions are filled in and saved.
1924
+
1925
+ Who may start:
1926
+
1927
+ - a free device: anyone who can see it;
1928
+
1929
+ - a device you already hold, or running your own Appium session: you (the preview runs
1930
+ alongside it);
1931
+
1932
+ - a device held by someone whose preview is no longer running (left over from a restart):
1933
+ anyone; the old hold is cleared;
1934
+
1935
+ - otherwise `409`, naming the holder. Admins may always start.
1936
+
1937
+ Needs the `MEMBER` role and the `devices` scope.
1938
+
1939
+ On a hub, for a node's phone, the request is forwarded to the node, which holds the phone by
1940
+ its own rules; on success the hub also marks the phone busy and held by you at once. A cloud
1941
+ provider's phone answers `501`.
1942
+ responses:
1943
+ '200':
1944
+ description: The preview is running. `type` says which transport to use.
1945
+ content:
1946
+ application/json:
1947
+ schema:
1948
+ $ref: '#/components/schemas/ControlStreamStartResult'
1949
+ examples:
1950
+ mjpeg:
1951
+ summary: MJPEG (iOS, or Android without H.264)
1952
+ value:
1953
+ success: true
1954
+ type: mjpeg
1955
+ mjpegPort: 9101
1956
+ streamUrl: /xenon/api/control/00008110-00084CE80E51401E/stream
1957
+ h264:
1958
+ summary: H.264 (Android, setting on)
1959
+ value:
1960
+ success: true
1961
+ type: h264
1962
+ h264Path: /xenon/api/control/R5CT32ABCDE/stream/h264
1963
+ '400':
1964
+ description: The udid is malformed.
1965
+ content:
1966
+ application/json:
1967
+ schema:
1968
+ $ref: '#/components/schemas/Error'
1969
+ examples:
1970
+ invalidUdid:
1971
+ summary: The udid segment is not valid percent-encoding
1972
+ value:
1973
+ success: false
1974
+ error: invalid_udid
1975
+ '401':
1976
+ description: No credential, or one that names no user.
1977
+ content:
1978
+ application/json:
1979
+ schema:
1980
+ $ref: '#/components/schemas/Error'
1981
+ example:
1982
+ success: false
1983
+ error: unauthenticated
1984
+ '403':
1985
+ $ref: '#/components/responses/ControlForbiddenMutation'
1986
+ '404':
1987
+ $ref: '#/components/responses/ControlDeviceNotFound'
1988
+ '409':
1989
+ $ref: '#/components/responses/ControlHeld'
1990
+ '429':
1991
+ $ref: '#/components/responses/RateLimited'
1992
+ '500':
1993
+ description: The preview could not be started (WebDriverAgent, scrcpy or the capture failed).
1994
+ content:
1995
+ application/json:
1996
+ schema:
1997
+ $ref: '#/components/schemas/Error'
1998
+ example:
1999
+ success: false
2000
+ error: WDA did not become ready on port 8101 within 60000ms
2001
+ '501':
2002
+ $ref: '#/components/responses/ControlCloudPhone'
2003
+ '502':
2004
+ $ref: '#/components/responses/ControlNodeUnreachable'
2005
+ '503':
2006
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
2007
+ '504':
2008
+ $ref: '#/components/responses/ControlNodeTimeout'
2009
+ /api/control/{udid}/stream/ticket:
2010
+ post:
2011
+ tags:
2012
+ - Control
2013
+ parameters:
2014
+ - $ref: '#/components/parameters/ControlUdid'
2015
+ operationId: createDeviceStreamTicket
2016
+ summary: Get a ticket to open the live preview
2017
+ description: >-
2018
+ Mints a single-use ticket, valid for 60 seconds and bound to this device and to you, for
2019
+ clients that cannot send headers or the cookie (an `<img>` tag, a WebSocket). It opens one
2020
+ of:
2021
+
2022
+ - `GET /api/control/{udid}/stream?ticket=…` (MJPEG);
2023
+
2024
+ - the WebSocket `/xenon/api/control/{udid}/stream/h264?ticket=…` (H.264 preview);
2025
+
2026
+ - the WebSocket `/xenon/api/control/{udid}/logcat?ticket=…` (live Android logs; it applies
2027
+ the same ownership rule as `GET logs`).
2028
+
2029
+ Mint a new ticket for each connection. Your team access is checked here, when the ticket is
2030
+ minted. Minting holds nothing and starts nothing; missing screen dimensions are filled in
2031
+ and saved in the background.
2032
+
2033
+ Needs the `MEMBER` role and the `devices` scope. Not refused for a held device: watching is
2034
+ a read.
2035
+
2036
+ On a hub the ticket is the hub's, for every phone (a node's included); the hub relays the
2037
+ node's stream or socket. For a cloud provider's phone the WebSockets close with `1008`.
2038
+ responses:
2039
+ '200':
2040
+ description: The ticket.
2041
+ content:
2042
+ application/json:
2043
+ schema:
2044
+ $ref: '#/components/schemas/ControlStreamTicket'
2045
+ example:
2046
+ ticket: >-
2047
+ eyJhbGciOiJSUzI1NiIsImtpZCI6Inhlbi0xIn0.eyJhdWQiOiJ4ZW5vbi1zdHJlYW0iLCJ1ZGlkIjoiUjVDVDMyQUJDREUifQ.c2ln
2048
+ expiresIn: 60
2049
+ '400':
2050
+ description: The udid is malformed.
2051
+ content:
2052
+ application/json:
2053
+ schema:
2054
+ $ref: '#/components/schemas/Error'
2055
+ examples:
2056
+ invalidUdid:
2057
+ summary: The udid segment is not valid percent-encoding
2058
+ value:
2059
+ success: false
2060
+ error: invalid_udid
2061
+ '401':
2062
+ description: No credential, or one that names no user.
2063
+ content:
2064
+ application/json:
2065
+ schema:
2066
+ $ref: '#/components/schemas/Error'
2067
+ example:
2068
+ error: unauthenticated
2069
+ '403':
2070
+ $ref: '#/components/responses/ControlForbiddenMutation'
2071
+ '404':
2072
+ $ref: '#/components/responses/ControlDeviceNotFound'
2073
+ '429':
2074
+ $ref: '#/components/responses/RateLimited'
2075
+ '503':
2076
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
2077
+ /api/control/{udid}/stream:
2078
+ get:
2079
+ tags:
2080
+ - Control
2081
+ parameters:
2082
+ - $ref: '#/components/parameters/ControlUdid'
2083
+ - in: query
2084
+ name: ticket
2085
+ required: false
2086
+ schema:
2087
+ type: string
2088
+ description: >-
2089
+ A single-use ticket from `POST /api/control/{udid}/stream/ticket`, in place of other
2090
+ credentials.
2091
+ example: >-
2092
+ eyJhbGciOiJSUzI1NiIsImtpZCI6Inhlbi0xIn0.eyJhdWQiOiJ4ZW5vbi1zdHJlYW0iLCJ1ZGlkIjoiUjVDVDMyQUJDREUifQ.c2ln
2093
+ operationId: streamDeviceScreen
2094
+ summary: Watch the device's screen (MJPEG)
2095
+ description: >-
2096
+ Streams the screen as MJPEG (`multipart/x-mixed-replace`) for as long as the connection
2097
+ stays open. Starts the device's MJPEG capture if it isn't running (iOS checks that
2098
+ WebDriverAgent still answers and restarts it if not), but holds nothing: use `POST
2099
+ stream/start` to hold the device. Each open connection counts as a viewer; a preview with no
2100
+ viewers is eventually stopped.
2101
+
2102
+
2103
+ Authenticate as usual, or with `?ticket=` from `POST stream/ticket`, for an `<img src>` that
2104
+ can carry neither headers nor the cookie. A ticket works only here, only for its device, and
2105
+ only once.
2106
+
2107
+
2108
+ Open to watchers: a busy device can be watched by anyone who can see it. Needs the `MEMBER`
2109
+ role; any scope will do.
2110
+
2111
+
2112
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
2113
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
2114
+ `501`. The stream is relayed for as long as you watch.
2115
+ security:
2116
+ - AccessKeyAuth: []
2117
+ TokenAuth: []
2118
+ - BearerAuth: []
2119
+ - CookieAuth: []
2120
+ - ControlStreamTicket: []
2121
+ responses:
2122
+ '200':
2123
+ description: The MJPEG stream, one JPEG per part, boundary `BoundaryString`.
2124
+ content:
2125
+ multipart/x-mixed-replace:
2126
+ schema:
2127
+ type: string
2128
+ format: binary
2129
+ '400':
2130
+ description: The udid is malformed.
2131
+ content:
2132
+ application/json:
2133
+ schema:
2134
+ $ref: '#/components/schemas/Error'
2135
+ examples:
2136
+ invalidUdid:
2137
+ summary: The udid segment is not valid percent-encoding
2138
+ value:
2139
+ success: false
2140
+ error: invalid_udid
2141
+ '401':
2142
+ description: No credential, or a ticket that is invalid, expired, already used or for another device.
2143
+ content:
2144
+ application/json:
2145
+ schema:
2146
+ $ref: '#/components/schemas/Error'
2147
+ examples:
2148
+ ticket:
2149
+ summary: A bad ticket
2150
+ value:
2151
+ error: invalid ticket
2152
+ none:
2153
+ summary: No credential
2154
+ value:
2155
+ error: unauthenticated
2156
+ '403':
2157
+ $ref: '#/components/responses/ControlForbiddenRead'
2158
+ '404':
2159
+ description: >-
2160
+ No such device (or one outside your teams), no MJPEG source for it, or the preview
2161
+ proxy could not be set up.
2162
+ content:
2163
+ application/json:
2164
+ schema:
2165
+ $ref: '#/components/schemas/Error'
2166
+ examples:
2167
+ device:
2168
+ summary: Unknown or hidden device
2169
+ value:
2170
+ error: not_found
2171
+ message: Device not found
2172
+ noSource:
2173
+ summary: No MJPEG source for the device
2174
+ value:
2175
+ error: MJPEG port not found for device
2176
+ hint: Start the preview with POST /stream/start, then open this stream again.
2177
+ noProxy:
2178
+ summary: The preview proxy could not be set up
2179
+ value:
2180
+ error: not_found
2181
+ message: The preview could not be started
2182
+ '429':
2183
+ $ref: '#/components/responses/RateLimited'
2184
+ '500':
2185
+ description: The stream proxy failed.
2186
+ content:
2187
+ application/json:
2188
+ schema:
2189
+ $ref: '#/components/schemas/Error'
2190
+ example:
2191
+ error: Stream proxy error
2192
+ message: connect ECONNREFUSED 127.0.0.1:9101
2193
+ '501':
2194
+ $ref: '#/components/responses/ControlCloudPhone'
2195
+ '502':
2196
+ $ref: '#/components/responses/ControlNodeUnreachable'
2197
+ '503':
2198
+ description: >-
2199
+ The capture could not be started, its source didn't answer in time, or the device's
2200
+ ownership could not be checked.
2201
+ content:
2202
+ application/json:
2203
+ schema:
2204
+ $ref: '#/components/schemas/Error'
2205
+ examples:
2206
+ ios:
2207
+ summary: iOS stream failed to start
2208
+ value:
2209
+ error: Stream not available
2210
+ message: WDA did not become ready on port 8101 within 60000ms
2211
+ android:
2212
+ summary: Android capture failed to start
2213
+ value:
2214
+ error: Android stream failed
2215
+ message: device offline
2216
+ ownership:
2217
+ summary: Lookup failed
2218
+ value:
2219
+ success: false
2220
+ error: device_ownership_unavailable
2221
+ message: Could not verify device ownership. Try again.
2222
+ text/html:
2223
+ schema:
2224
+ type: string
2225
+ example: '[MjpegProxy] Source not available after timeout'
2226
+ '504':
2227
+ $ref: '#/components/responses/ControlNodeTimeout'
2228
+ /api/control/{udid}/stream/status:
2229
+ get:
2230
+ tags:
2231
+ - Control
2232
+ parameters:
2233
+ - $ref: '#/components/parameters/ControlUdid'
2234
+ operationId: getDevicePreviewStatus
2235
+ summary: Get the live preview's status
2236
+ description: >-
2237
+ Says whether the device's preview is running and which transport a player should use
2238
+ (`type`: `h264` on Android when the server's `streaming.androidH264` setting is on and the
2239
+ device isn't being recorded, else `mjpeg`). A device with no preview answers `200` with
2240
+ `status: stopped`. iOS adds WebDriverAgent's port, the start time and the last error.
2241
+
2242
+
2243
+ Open to watchers. Needs the `MEMBER` role; any scope will do.
2244
+
2245
+
2246
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
2247
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
2248
+ `501`.
2249
+ responses:
2250
+ '200':
2251
+ description: The status.
2252
+ content:
2253
+ application/json:
2254
+ schema:
2255
+ $ref: '#/components/schemas/ControlStreamStatus'
2256
+ examples:
2257
+ ios:
2258
+ summary: iOS, running
2259
+ value:
2260
+ udid: 00008110-00084CE80E51401E
2261
+ status: running
2262
+ type: mjpeg
2263
+ wdaPort: 8101
2264
+ mjpegPort: 9101
2265
+ startedAt: 1759586400123
2266
+ h264:
2267
+ summary: Android, H.264
2268
+ value:
2269
+ udid: R5CT32ABCDE
2270
+ status: running
2271
+ type: h264
2272
+ h264Path: /xenon/api/control/R5CT32ABCDE/stream/h264
2273
+ mjpegPort: null
2274
+ stopped:
2275
+ summary: No preview
2276
+ value:
2277
+ udid: R5CT32ABCDE
2278
+ status: stopped
2279
+ type: mjpeg
2280
+ '400':
2281
+ description: The udid is malformed.
2282
+ content:
2283
+ application/json:
2284
+ schema:
2285
+ $ref: '#/components/schemas/Error'
2286
+ examples:
2287
+ invalidUdid:
2288
+ summary: The udid segment is not valid percent-encoding
2289
+ value:
2290
+ success: false
2291
+ error: invalid_udid
2292
+ '401':
2293
+ $ref: '#/components/responses/Unauthorized'
2294
+ '403':
2295
+ $ref: '#/components/responses/ControlForbiddenRead'
2296
+ '404':
2297
+ $ref: '#/components/responses/ControlDeviceNotFound'
2298
+ '429':
2299
+ $ref: '#/components/responses/RateLimited'
2300
+ '501':
2301
+ $ref: '#/components/responses/ControlCloudPhone'
2302
+ '502':
2303
+ $ref: '#/components/responses/ControlNodeUnreachable'
2304
+ '503':
2305
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
2306
+ '504':
2307
+ $ref: '#/components/responses/ControlNodeTimeout'
2308
+ /api/control/{udid}/stream/leave:
2309
+ post:
2310
+ tags:
2311
+ - Control
2312
+ parameters:
2313
+ - $ref: '#/components/parameters/ControlUdid'
2314
+ operationId: leaveDevicePreview
2315
+ summary: Stop watching the preview
2316
+ description: >-
2317
+ Tells the server a page stopped watching (navigated away, closed, reloaded). Nothing stops
2318
+ at once: after a 3-second grace the preview is stopped and your hold released only if nobody
2319
+ is watching it and no recording reads it. A `stream/start` within the grace (a reload)
2320
+ cancels it. Use this rather than `stream/stop` when other tabs may be watching.
2321
+
2322
+
2323
+ You may leave a device you hold, one with a legacy hold, or any device as an admin; another
2324
+ user's hold answers `403`.
2325
+
2326
+
2327
+ Needs the `MEMBER` role and the `devices` scope.
2328
+
2329
+
2330
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
2331
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
2332
+ `501`.
2333
+ responses:
2334
+ '202':
2335
+ description: Noted; the preview will stop after the grace if nobody is watching.
2336
+ content:
2337
+ application/json:
2338
+ schema:
2339
+ $ref: '#/components/schemas/ControlStreamLeaveResult'
2340
+ example:
2341
+ success: true
2342
+ pending: true
2343
+ '400':
2344
+ description: The udid is malformed.
2345
+ content:
2346
+ application/json:
2347
+ schema:
2348
+ $ref: '#/components/schemas/Error'
2349
+ examples:
2350
+ invalidUdid:
2351
+ summary: The udid segment is not valid percent-encoding
2352
+ value:
2353
+ success: false
2354
+ error: invalid_udid
2355
+ '401':
2356
+ $ref: '#/components/responses/Unauthorized'
2357
+ '403':
2358
+ description: >-
2359
+ Another user holds the device; or the credential lacks the `devices` scope, or a browser
2360
+ request failed the same-origin check.
2361
+ content:
2362
+ application/json:
2363
+ schema:
2364
+ $ref: '#/components/schemas/Error'
2365
+ examples:
2366
+ held:
2367
+ summary: Another user's hold
2368
+ value:
2369
+ success: false
2370
+ error: lock_owned_by_another_user
2371
+ message: This device is being controlled by another user.
2372
+ scope:
2373
+ summary: No devices scope
2374
+ value:
2375
+ error: insufficient scope
2376
+ '404':
2377
+ $ref: '#/components/responses/ControlDeviceNotFound'
2378
+ '429':
2379
+ $ref: '#/components/responses/RateLimited'
2380
+ '501':
2381
+ $ref: '#/components/responses/ControlCloudPhone'
2382
+ '502':
2383
+ $ref: '#/components/responses/ControlNodeUnreachable'
2384
+ '503':
2385
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
2386
+ '504':
2387
+ $ref: '#/components/responses/ControlNodeTimeout'
2388
+ /api/control/{udid}/stream/stop:
2389
+ post:
2390
+ tags:
2391
+ - Control
2392
+ parameters:
2393
+ - $ref: '#/components/parameters/ControlUdid'
2394
+ operationId: stopDevicePreview
2395
+ summary: Stop the preview and release the device
2396
+ description: >-
2397
+ Stops the device's preview on every transport at once and releases its preview hold,
2398
+ including a hold left over from a restart. Every other tab watching the device loses its
2399
+ preview too; prefer `stream/leave`. On iOS the stream is kept while an Appium session on the
2400
+ phone may be using its WebDriverAgent.
2401
+
2402
+
2403
+ You may stop a device you hold, one with a legacy hold, or (as an admin) any device, which
2404
+ force-releases another user's hold. Refused with `409 device_recording` while the device is
2405
+ being recorded: stop the recording instead (`POST /api/recordings/{groupId}/stop`).
2406
+
2407
+
2408
+ Needs the `MEMBER` role and the `devices` scope.
2409
+
2410
+
2411
+ On a hub, for a node's phone, the request is sent on to that node once (never retried),
2412
+ signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
2413
+ `501`. The hub also refuses with `409 device_recording` while it records the node's phone.
2414
+ responses:
2415
+ '200':
2416
+ description: Stopped.
2417
+ content:
2418
+ application/json:
2419
+ schema:
2420
+ $ref: '#/components/schemas/Success'
2421
+ example:
2422
+ success: true
2423
+ '400':
2424
+ description: The udid is malformed.
2425
+ content:
2426
+ application/json:
2427
+ schema:
2428
+ $ref: '#/components/schemas/Error'
2429
+ examples:
2430
+ invalidUdid:
2431
+ summary: The udid segment is not valid percent-encoding
2432
+ value:
2433
+ success: false
2434
+ error: invalid_udid
2435
+ '401':
2436
+ $ref: '#/components/responses/Unauthorized'
2437
+ '403':
2438
+ description: >-
2439
+ Another user holds the device and you aren't an admin; or the credential lacks the
2440
+ `devices` scope, or a browser request failed the same-origin check.
2441
+ content:
2442
+ application/json:
2443
+ schema:
2444
+ $ref: '#/components/schemas/Error'
2445
+ examples:
2446
+ held:
2447
+ summary: Another user's hold
2448
+ value:
2449
+ success: false
2450
+ error: lock_owned_by_another_user
2451
+ message: >-
2452
+ This device is being controlled by another user. Ask them to stop, or use an
2453
+ admin key to force-release.
2454
+ scope:
2455
+ summary: No devices scope
2456
+ value:
2457
+ error: insufficient scope
2458
+ '404':
2459
+ $ref: '#/components/responses/ControlDeviceNotFound'
2460
+ '409':
2461
+ description: The device is being recorded.
2462
+ content:
2463
+ application/json:
2464
+ schema:
2465
+ $ref: '#/components/schemas/Error'
2466
+ example:
2467
+ success: false
2468
+ error: device_recording
2469
+ message: This device is being recorded. Stop the recording first.
2470
+ groupId: 7d2e9b4a-5c1f-4e8b-a3d6-0f9c2b7e1a54
2471
+ '429':
2472
+ $ref: '#/components/responses/RateLimited'
2473
+ '500':
2474
+ description: Stopping failed.
2475
+ content:
2476
+ application/json:
2477
+ schema:
2478
+ $ref: '#/components/schemas/Error'
2479
+ example:
2480
+ success: false
2481
+ error: kill ESRCH
2482
+ '501':
2483
+ $ref: '#/components/responses/ControlCloudPhone'
2484
+ '502':
2485
+ $ref: '#/components/responses/ControlNodeUnreachable'
2486
+ '503':
2487
+ $ref: '#/components/responses/ControlOwnershipUnavailable'
2488
+ '504':
2489
+ $ref: '#/components/responses/ControlNodeTimeout'
2490
+ components:
2491
+ parameters:
2492
+ ControlUdid:
2493
+ in: path
2494
+ name: udid
2495
+ required: true
2496
+ schema:
2497
+ type: string
2498
+ description: The device udid (URL-encoded).
2499
+ example: 00008110-00084CE80E51401E
2500
+ securitySchemes:
2501
+ ControlStreamTicket:
2502
+ type: apiKey
2503
+ in: query
2504
+ name: ticket
2505
+ description: >-
2506
+ A single-use, 60-second ticket from `POST /api/control/{udid}/stream/ticket`. Accepted only
2507
+ by `GET /api/control/{udid}/stream` (and the preview and logcat WebSockets) for the device
2508
+ it was minted for.
2509
+ responses:
2510
+ ControlDeviceNotFound:
2511
+ description: No such device, or it is outside your teams (the two answer the same).
2512
+ content:
2513
+ application/json:
2514
+ schema:
2515
+ $ref: '#/components/schemas/Error'
2516
+ example:
2517
+ error: not_found
2518
+ message: Device not found
2519
+ ControlForbiddenMutation:
2520
+ description: >-
2521
+ The credential lacks the `devices` scope or the `MEMBER` role, or a request with the
2522
+ dashboard cookie failed the same-origin check.
2523
+ content:
2524
+ application/json:
2525
+ schema:
2526
+ $ref: '#/components/schemas/Error'
2527
+ examples:
2528
+ scope:
2529
+ summary: No devices scope
2530
+ value:
2531
+ error: insufficient scope
2532
+ role:
2533
+ summary: Role below MEMBER
2534
+ value:
2535
+ error: requires role >= MEMBER
2536
+ csrf:
2537
+ summary: Cross-site browser request
2538
+ value:
2539
+ error: 'CSRF: Origin/Referer mismatch'
2540
+ ControlForbiddenRead:
2541
+ description: The caller's role is below `MEMBER`.
2542
+ content:
2543
+ application/json:
2544
+ schema:
2545
+ $ref: '#/components/schemas/Error'
2546
+ example:
2547
+ error: requires role >= MEMBER
2548
+ ControlHeld:
2549
+ description: >-
2550
+ Another user holds the device (their preview or recording), or it runs another user's Appium
2551
+ session. Admins are never refused. On a hub, a node's refusal is relayed with the holder's
2552
+ name filled in.
2553
+ content:
2554
+ application/json:
2555
+ schema:
2556
+ $ref: '#/components/schemas/ControlDenied'
2557
+ examples:
2558
+ held:
2559
+ summary: Another user's preview or recording
2560
+ value:
2561
+ success: false
2562
+ error: device_held_by_another_user
2563
+ message: >-
2564
+ Device is being controlled by Priya Raman. Ask them to release it, or use an admin
2565
+ key to force-release.
2566
+ holder:
2567
+ userId: c7a1e5d2-3f4b-4a9e-8c21-5b6d7e8f9a01
2568
+ name: Priya Raman
2569
+ session:
2570
+ summary: Another user's Appium session
2571
+ value:
2572
+ success: false
2573
+ error: device_in_use_by_session
2574
+ message: Device is in use by an Appium session owned by Priya Raman.
2575
+ holder:
2576
+ userId: c7a1e5d2-3f4b-4a9e-8c21-5b6d7e8f9a01
2577
+ name: Priya Raman
2578
+ ControlOwnershipUnavailable:
2579
+ description: >-
2580
+ The device or its session's owner couldn't be looked up, so access couldn't be checked.
2581
+ Never treated as allowed; try again.
2582
+ content:
2583
+ application/json:
2584
+ schema:
2585
+ $ref: '#/components/schemas/Error'
2586
+ example:
2587
+ success: false
2588
+ error: device_ownership_unavailable
2589
+ message: Could not verify device ownership. Try again.
2590
+ ControlCloudPhone:
2591
+ description: 'On a hub: the phone is a cloud provider''s, and device control isn''t available for it.'
2592
+ content:
2593
+ application/json:
2594
+ schema:
2595
+ $ref: '#/components/schemas/Error'
2596
+ example:
2597
+ success: false
2598
+ error: not_available_for_cloud_phone
2599
+ message: This phone is a cloud provider's. Device control isn't available for it here.
2600
+ ControlNodeUnreachable:
2601
+ description: 'On a hub: the phone''s node couldn''t be reached.'
2602
+ content:
2603
+ application/json:
2604
+ schema:
2605
+ $ref: '#/components/schemas/Error'
2606
+ example:
2607
+ success: false
2608
+ error: node_unreachable
2609
+ message: 'Xenon could not reach node http://10.0.4.21:4723 for this phone: ECONNREFUSED'
2610
+ ControlNodeTimeout:
2611
+ description: 'On a hub: the phone''s node didn''t start answering in time (60 s; 15 minutes for an install).'
2612
+ content:
2613
+ application/json:
2614
+ schema:
2615
+ $ref: '#/components/schemas/Error'
2616
+ example:
2617
+ success: false
2618
+ error: node_timeout
2619
+ message: Node http://10.0.4.21:4723 didn't answer within 60 s.
2620
+ schemas:
2621
+ ControlPoint:
2622
+ type: object
2623
+ required:
2624
+ - x
2625
+ - 'y'
2626
+ properties:
2627
+ x:
2628
+ type: number
2629
+ description: Horizontal position in screen points.
2630
+ example: 196
2631
+ 'y':
2632
+ type: number
2633
+ description: Vertical position in screen points.
2634
+ example: 420
2635
+ ControlSwipeRequest:
2636
+ type: object
2637
+ required:
2638
+ - x
2639
+ - 'y'
2640
+ - endX
2641
+ - endY
2642
+ properties:
2643
+ x:
2644
+ type: number
2645
+ description: Start, horizontal.
2646
+ example: 196
2647
+ 'y':
2648
+ type: number
2649
+ description: Start, vertical.
2650
+ example: 700
2651
+ endX:
2652
+ type: number
2653
+ description: End, horizontal.
2654
+ example: 196
2655
+ endY:
2656
+ type: number
2657
+ description: End, vertical.
2658
+ example: 200
2659
+ duration:
2660
+ type: number
2661
+ default: 1000
2662
+ description: Milliseconds.
2663
+ example: 300
2664
+ ControlTouchAndHoldRequest:
2665
+ type: object
2666
+ required:
2667
+ - x
2668
+ - 'y'
2669
+ properties:
2670
+ x:
2671
+ type: number
2672
+ description: Horizontal position in screen points.
2673
+ example: 196
2674
+ 'y':
2675
+ type: number
2676
+ description: Vertical position in screen points.
2677
+ example: 420
2678
+ duration:
2679
+ type: number
2680
+ default: 1000
2681
+ description: How long to hold, in milliseconds.
2682
+ example: 1500
2683
+ ControlTextRequest:
2684
+ type: object
2685
+ required:
2686
+ - text
2687
+ properties:
2688
+ text:
2689
+ type: string
2690
+ example: qa.user@example.com
2691
+ ControlKeyEventRequest:
2692
+ type: object
2693
+ required:
2694
+ - keyCode
2695
+ properties:
2696
+ keyCode:
2697
+ oneOf:
2698
+ - type: integer
2699
+ - type: string
2700
+ description: >-
2701
+ Android: a key code number or `KEYCODE_…` name. iOS: a key or button name (`home`,
2702
+ `volumeUp`, `enter`, `backspace`…).
2703
+ example: 4
2704
+ ControlScreenshot:
2705
+ type: object
2706
+ properties:
2707
+ screenshot:
2708
+ type: string
2709
+ format: byte
2710
+ description: The image, base64 (PNG, or JPEG from a running Android preview).
2711
+ ControlDisplayState:
2712
+ type: object
2713
+ properties:
2714
+ state:
2715
+ type: string
2716
+ enum:
2717
+ - 'on'
2718
+ - 'off'
2719
+ - doze
2720
+ - unknown
2721
+ description: '`doze` is an always-on display; `unknown` means it could not be read.'
2722
+ ControlClipboard:
2723
+ type: object
2724
+ required:
2725
+ - content
2726
+ properties:
2727
+ content:
2728
+ type: string
2729
+ example: https://example.com/reset?code=4821
2730
+ ControlInstallPathRequest:
2731
+ type: object
2732
+ required:
2733
+ - appPath
2734
+ properties:
2735
+ appPath:
2736
+ type: string
2737
+ description: A path on this server's file system.
2738
+ example: /opt/builds/shop-2.14.0-debug.apk
2739
+ ControlInstallLibraryAppRequest:
2740
+ type: object
2741
+ required:
2742
+ - appId
2743
+ properties:
2744
+ appId:
2745
+ type: string
2746
+ description: The app's id in the app library.
2747
+ example: b3c1f0e2-6a8d-4b6e-9a51-2f7d0c9e4a13
2748
+ ControlUploadInstallRequest:
2749
+ type: object
2750
+ required:
2751
+ - app
2752
+ properties:
2753
+ app:
2754
+ type: string
2755
+ format: binary
2756
+ description: >-
2757
+ The app: `.apk` for Android, `.ipa` or zipped `.app` for iOS. At most 4 GiB. The file
2758
+ name's extension is kept.
2759
+ ControlInstallResult:
2760
+ type: object
2761
+ properties:
2762
+ success:
2763
+ type: boolean
2764
+ example: true
2765
+ message:
2766
+ type: string
2767
+ example: Installed Shop
2768
+ ControlUninstallRequest:
2769
+ type: object
2770
+ required:
2771
+ - bundleId
2772
+ properties:
2773
+ bundleId:
2774
+ type: string
2775
+ description: Package name (Android) or bundle id (iOS).
2776
+ example: com.example.shop
2777
+ ControlLogs:
2778
+ type: object
2779
+ properties:
2780
+ logs:
2781
+ type: string
2782
+ ControlShellRequest:
2783
+ type: object
2784
+ required:
2785
+ - command
2786
+ properties:
2787
+ command:
2788
+ type: string
2789
+ description: One allowlisted command (see the description).
2790
+ example: dumpsys battery
2791
+ ControlShellResult:
2792
+ type: object
2793
+ description: Exactly one of `output` and `error`.
2794
+ properties:
2795
+ output:
2796
+ type: string
2797
+ error:
2798
+ type: string
2799
+ description: Why the command was refused or failed.
2800
+ ControlStatusError:
2801
+ type: object
2802
+ properties:
2803
+ status:
2804
+ type: string
2805
+ enum:
2806
+ - error
2807
+ message:
2808
+ type: string
2809
+ example: OCR worker failed to start
2810
+ ControlRect:
2811
+ type: object
2812
+ properties:
2813
+ x:
2814
+ type: number
2815
+ example: 142
2816
+ 'y':
2817
+ type: number
2818
+ example: 210
2819
+ width:
2820
+ type: number
2821
+ example: 46
2822
+ height:
2823
+ type: number
2824
+ example: 26
2825
+ ControlInspectorNode:
2826
+ type: object
2827
+ description: One element of the UI tree.
2828
+ properties:
2829
+ name:
2830
+ type: string
2831
+ example: Sign in
2832
+ type:
2833
+ type: string
2834
+ example: XCUIElementTypeButton
2835
+ text:
2836
+ type: string
2837
+ label:
2838
+ type: string
2839
+ value:
2840
+ type: string
2841
+ enabled:
2842
+ type: boolean
2843
+ visible:
2844
+ type: boolean
2845
+ rect:
2846
+ $ref: '#/components/schemas/ControlRect'
2847
+ xpath:
2848
+ type: string
2849
+ example: //XCUIElementTypeButton[@name="Sign in"]
2850
+ suggestedLocators:
2851
+ type: array
2852
+ items:
2853
+ type: object
2854
+ properties:
2855
+ strategy:
2856
+ type: string
2857
+ example: accessibility id
2858
+ value:
2859
+ type: string
2860
+ example: Sign in
2861
+ unique:
2862
+ type: boolean
2863
+ example: true
2864
+ score:
2865
+ type: integer
2866
+ minimum: 0
2867
+ maximum: 100
2868
+ description: Robustness, 0 to 100.
2869
+ example: 95
2870
+ suggestedActions:
2871
+ type: array
2872
+ items:
2873
+ type: object
2874
+ properties:
2875
+ action:
2876
+ type: string
2877
+ example: click
2878
+ snippet:
2879
+ type: string
2880
+ example: await driver.$('~Sign in').click();
2881
+ description:
2882
+ type: string
2883
+ example: Tap the button
2884
+ children:
2885
+ type: array
2886
+ items:
2887
+ $ref: '#/components/schemas/ControlInspectorNode'
2888
+ attributes:
2889
+ type: object
2890
+ additionalProperties: true
2891
+ ControlInspectorSnapshot:
2892
+ type: object
2893
+ properties:
2894
+ udid:
2895
+ type: string
2896
+ example: 00008110-00084CE80E51401E
2897
+ platform:
2898
+ type: string
2899
+ example: ios
2900
+ timestamp:
2901
+ type: string
2902
+ format: date-time
2903
+ example: '2026-10-04T14:05:22.118Z'
2904
+ screenshot:
2905
+ type: string
2906
+ format: byte
2907
+ description: The screenshot, base64.
2908
+ hierarchy:
2909
+ $ref: '#/components/schemas/ControlInspectorNode'
2910
+ hierarchySource:
2911
+ type: string
2912
+ enum:
2913
+ - appium-session
2914
+ - device
2915
+ description: '`appium-session` when the tree is a running session''s page source.'
2916
+ sessionId:
2917
+ type: string
2918
+ nullable: true
2919
+ description: The session the tree came from; null when read from the device.
2920
+ example: null
2921
+ metadata:
2922
+ type: object
2923
+ properties:
2924
+ screenWidth:
2925
+ type: integer
2926
+ example: 393
2927
+ screenHeight:
2928
+ type: integer
2929
+ example: 852
2930
+ ControlOmniScanResult:
2931
+ type: object
2932
+ properties:
2933
+ status:
2934
+ type: string
2935
+ enum:
2936
+ - success
2937
+ value:
2938
+ type: object
2939
+ description: The analysis.
2940
+ properties:
2941
+ timestamp:
2942
+ type: string
2943
+ format: date-time
2944
+ ocr:
2945
+ type: object
2946
+ properties:
2947
+ text:
2948
+ type: string
2949
+ words:
2950
+ type: array
2951
+ items:
2952
+ type: object
2953
+ properties:
2954
+ text:
2955
+ type: string
2956
+ confidence:
2957
+ type: number
2958
+ description: 0 to 100.
2959
+ bbox:
2960
+ type: object
2961
+ properties:
2962
+ x0:
2963
+ type: number
2964
+ y0:
2965
+ type: number
2966
+ x1:
2967
+ type: number
2968
+ y1:
2969
+ type: number
2970
+ ai_insights:
2971
+ type: object
2972
+ nullable: true
2973
+ additionalProperties: true
2974
+ description: The AI provider's analysis; null if it was skipped or failed.
2975
+ ControlTestLocatorRequest:
2976
+ type: object
2977
+ required:
2978
+ - strategy
2979
+ - selector
2980
+ properties:
2981
+ strategy:
2982
+ type: string
2983
+ enum:
2984
+ - '-custom:ai-text'
2985
+ - '-custom:ai-icon'
2986
+ example: '-custom:ai-text'
2987
+ selector:
2988
+ type: string
2989
+ description: The text to find, or a description of the icon.
2990
+ example: Sign in
2991
+ ControlTestLocatorResult:
2992
+ type: object
2993
+ properties:
2994
+ status:
2995
+ type: string
2996
+ enum:
2997
+ - success
2998
+ value:
2999
+ type: array
3000
+ items:
3001
+ type: object
3002
+ properties:
3003
+ id:
3004
+ type: string
3005
+ description: A virtual element id.
3006
+ example: omni_ocr_1759586722118_k3j9x
3007
+ text:
3008
+ type: string
3009
+ description: The matched word (`ai-text` only).
3010
+ rect:
3011
+ $ref: '#/components/schemas/ControlRect'
3012
+ confidence:
3013
+ type: number
3014
+ description: 0 to 1.
3015
+ example: 0.962
3016
+ ControlAppiumSession:
3017
+ type: object
3018
+ properties:
3019
+ status:
3020
+ type: string
3021
+ enum:
3022
+ - success
3023
+ sessionId:
3024
+ type: string
3025
+ nullable: true
3026
+ description: The Appium session driving the device; null when none is.
3027
+ basePath:
3028
+ type: string
3029
+ description: This server's WebDriver base path (`''` for the root).
3030
+ example: /wd/hub
3031
+ ControlStreamStartResult:
3032
+ type: object
3033
+ properties:
3034
+ success:
3035
+ type: boolean
3036
+ example: true
3037
+ type:
3038
+ type: string
3039
+ enum:
3040
+ - mjpeg
3041
+ - h264
3042
+ mjpegPort:
3043
+ type: integer
3044
+ description: 'MJPEG only: the local capture port.'
3045
+ example: 9101
3046
+ streamUrl:
3047
+ type: string
3048
+ description: 'MJPEG only: where to watch.'
3049
+ example: /xenon/api/control/00008110-00084CE80E51401E/stream
3050
+ h264Path:
3051
+ type: string
3052
+ description: 'H.264 only: the WebSocket path (open it with a ticket).'
3053
+ example: /xenon/api/control/R5CT32ABCDE/stream/h264
3054
+ ControlStreamTicket:
3055
+ type: object
3056
+ properties:
3057
+ ticket:
3058
+ type: string
3059
+ description: Single use, bound to this device and to you.
3060
+ expiresIn:
3061
+ type: integer
3062
+ description: Seconds.
3063
+ example: 60
3064
+ ControlStreamStatus:
3065
+ type: object
3066
+ properties:
3067
+ udid:
3068
+ type: string
3069
+ example: 00008110-00084CE80E51401E
3070
+ status:
3071
+ type: string
3072
+ example: running
3073
+ description: >-
3074
+ `stopped` when no preview runs; otherwise the stream's state (`starting`, `running`,
3075
+ `error`, …).
3076
+ type:
3077
+ type: string
3078
+ enum:
3079
+ - mjpeg
3080
+ - h264
3081
+ description: The transport a player should use.
3082
+ h264Path:
3083
+ type: string
3084
+ description: Android with H.264 only.
3085
+ mjpegPort:
3086
+ type: integer
3087
+ nullable: true
3088
+ wdaPort:
3089
+ type: integer
3090
+ description: iOS only.
3091
+ startedAt:
3092
+ type: integer
3093
+ description: iOS only. Epoch milliseconds.
3094
+ lastError:
3095
+ type: string
3096
+ description: iOS only.
3097
+ ControlStreamLeaveResult:
3098
+ type: object
3099
+ properties:
3100
+ success:
3101
+ type: boolean
3102
+ example: true
3103
+ pending:
3104
+ type: boolean
3105
+ example: true
3106
+ ControlDenied:
3107
+ type: object
3108
+ required:
3109
+ - error
3110
+ properties:
3111
+ success:
3112
+ type: boolean
3113
+ example: false
3114
+ error:
3115
+ type: string
3116
+ enum:
3117
+ - device_held_by_another_user
3118
+ - device_in_use_by_session
3119
+ message:
3120
+ type: string
3121
+ holder:
3122
+ type: object
3123
+ description: Who holds the device, when known.
3124
+ properties:
3125
+ userId:
3126
+ type: string
3127
+ name:
3128
+ type: string
3129
+ description: The holder's name, when it could be looked up.