Perform one action. The body is a single object discriminated by type.
Every response carries device_ack. 200 with device_ack: true means the device confirmed it accepted the command. 202 with device_ack: false means the command was handed over and the outcome could not be confirmed, because the hardware serving that phone is mid-update; verify with a snapshot.
What `device_ack: true` is worth. It is the device saying it accepted the command. It is *not* a claim that the screen changed, and nothing can observe that for you.
For a gesture it is worth noticeably less, and this document used to say the opposite. A tap either lands or is refused. A swipe, a drag, a swipe_direction, a scroll and the flick inside app_close are each a press, a walk and a release, and every one of those can be accepted in full while the screen does not move at all: the phone has one pointer, it is shared with whatever else is driving that handset, and how far a walk actually travels is decided by hardware we can ask but not watch. So every gesture answers effect_observed: false beside its acknowledgement, and that field is the honest one. Read the screen back with `GET /devices/{id}/snapshot` when a gesture matters, rather than treating a 200 as movement.
For anything whose work continues after the call returns the acknowledgement is weaker still, and recast is the clear example: it answers device_ack: true in under a second while the phone may take another minute to come back, or never come back.
One thing an ack cannot tell you is *where* a tap landed. It is uncommon, but some handsets occasionally place one away from where it was aimed, and an unattended run that carries on from the wrong screen is the expensive version of that. If you see it, send calibrate and repeat the tap.
On a fleet where every box reports outcomes a 200 always carries device_ack: true. The field earns its place by being false on a 202, and by giving you one thing to branch on rather than two status codes to remember.
An action against a phone that is not casting is refused with 503 device_offline rather than accepted. A phone that has stopped mirroring will still take a command and report success for work that never happened, so this endpoint checks first. Send recast, then retry.
Some verbs need a helper that runs on the phone, and some do not. This is the single most useful thing to know about failures here.
Taps, gestures, buttons, snapshots, calibrate and recast are driven through the handset's pointer and work on any phone that is casting. These verbs instead go through a small helper installed on the phone itself: text (beyond short plain ASCII, which goes over the keyboard), clipboard_get, clipboard_set, open_url, wifi, cellular, airplane, torch, brightness and device_ip. They are slower (several seconds is normal) and they can fail on a phone where everything else works.
When the helper does not answer in time you get 409 device_not_provisioned_for_action. The name is misleading and we are stuck with it for now (it is a published code that clients branch on): it reports a *timeout*, not a provisioning state. Read it as "the helper did not answer", not as "this key lacks a permission" and not as "this phone needs enabling for this one verb". There is no per-verb provisioning, no scope that unlocks it, and nothing on the key-creation screen that affects it: a key with devices.control can already send every verb on this list.
Tell the two cases apart by trying once more, a minute later:
- It succeeds. The helper was busy, often with something else driving the same phone. Nothing is wrong.
- It refuses again. Stop, and send us the X-Request-Id and the phone's inventory_id. Retrying will not clear it and nothing on your key affects it.
Do not build a retry loop around this code. A phone whose helper is down answers the same way every time, and an unattended loop against a live account is exactly where this becomes expensive. Two attempts is the whole strategy.
⚠️ `open_url` has a first-run cost per phone and per destination, and this is worth understanding before you build on it. iOS asks the person holding the phone to approve each destination the first time that phone is asked to open it (*Allow "ALI Remote" to open "Instagram"?*). Until that prompt is answered the request simply waits, and at thirty seconds the box gives up and answers device_not_provisioned_for_action.
Measured on a live handset: first call to a new destination 30 s, refused; once approved, the same call is about 2 s and stays that way. The approval is remembered per phone and per destination, so a phone that opens Instagram in two seconds will still spend thirty being refused the first time you ask it for TikTok.
Boxes on current builds answer that prompt themselves, so the first open_url to a new destination just runs a few seconds longer than every later one. On a box that has not picked that up yet the first call is still refused at thirty seconds; if the same phone and link keep refusing, tell us both and we will approve it at the rack. In the meantime press home plus a tap read off a snapshot reaches any app on the home screen in about a second, with no first-run cost.
A practical consequence worth planning for: because these verbs share one dependency, one of them failing predicts the rest. If you need to know whether a phone's helper is answering before you drive an account with it, send a clipboard_get first: it reads and changes nothing on the phone. It speaks for the helper, not for any one verb: open_url carries its per-destination approval on top, so a passing preflight promises the tier open_url runs on, not open_url itself.
send_binding_key is not part of the control surface and is listed here only because the catalogue is generated from the schema that validates it. It is a one-time step in preparing a handset, performed by ALI staff while somebody is watching that phone: it answers 403 forbidden on any customer credential, whatever scopes that credential holds, because the check is against the account behind it rather than against the key. There is nothing to reach for here in any case, since everything it prepares a phone for already has a verb of its own (text, the clipboard pair, the radio switches).
Swipes are one gesture, drawn the way a hand draws it. Every gesture here is the one the dashboard's arrows send: the finger is placed, pressed, rested, walked in sixteen steps and released at the end point, a little over half a second in all. swipe and drag take the finger from (x1,y1) to (x2,y2), and differ only in whether the content is allowed to keep moving afterwards. swipe_direction names where the finger goes: up drags from the bottom of the screen towards the top, which scrolls the content down. It runs from 80 % to 20 % of the axis by default, centred on the other one; distance shortens or lengthens that, as a fraction of the axis or as { "px": N } in snapshot pixels, always centred on the screen and never within 5 % of an edge; it needs the phone to have reported its screen size, and is refused rather than quietly drawn at the default page while it has not. scroll names where the view goes: it starts the finger at x,y and moves it against dy, so a positive dy scrolls down and a negative one scrolls up, by exactly that many snapshot pixels. Anchored close to an edge the whole line slides inwards rather than being cut short, so the finger always travels the full dy. A dy of 0 is refused rather than treated as a no-op: a finger that presses and lifts without travelling is a tap on whatever is under x,y.
`precise` decides whether that line is drawn twice the same way. A hand does not scroll a phone from the same pixel at the same speed sixty times in a row, so a swipe we lay out ourselves is varied: the whole line sits 5 to 15 % off to one side of where it would have been (sideways for an up or down swipe, up or down for a sideways one), each end moves along the axis by 5 to 15 % of the travel, and the walk runs 5 to 15 % faster or slower. The shape does not change: same direction, same sixteen steps, both ends still at least 5 % clear of the edges iOS owns. Only where it is drawn, and how quickly.
The default follows one rule: if you gave us the numbers we draw them, and if you only asked us for a page we draw it like a hand. So swipe, drag and scroll are exact, because their coordinates are yours and an icon has to land where you put it, and so is a swipe_direction carrying a distance, because that is a measurement you gave us and one we refuse to guess at without a screen. A swipe_direction with no distance names nothing but a direction, so the whole line is ours and it is varied. precise: true turns the variation off, precise: false turns it on, on any of the four. Varying a swipe is measured against the phone's screen size, so precise: false is refused with 503 device_offline on a phone that has not reported one yet, exactly as distance is; the default silently draws the box's own page instead, since that is a swipe drawn a little differently rather than one that did not happen. The dashboard varies every swipe it sends and offers no switch: nobody driving a phone by hand is trying to hit a coordinate.
Deprecated: `duration_ms`, `span`, `steps`, `step_ms`, `stepping`, `step_sleep` and `brake`. These pacing and sizing knobs are still accepted on swipe, drag and swipe_direction for one release so existing scripts keep working, and they are ignored: every gesture takes the walk described above, which is the one that measurably lands, varied only as precise decides. They will be refused in the release after this one. Use distance on swipe_direction where span was used.
calibrate puts the phone's pointer back in a known place. It is the one verb here that fixes a class of problem rather than doing something: a tap that lands off-target takes an unattended run somewhere it cannot recover from, and re-centring is the remedy. Send it when you see that happening, or once at the top of a sequence that will tap a lot. Not before every tap. It is a round trip of its own, about a second, and one action off the budget, so a tap-calibrate pair doubles both for a fault most handsets do not have. It changes nothing on screen, so a screenshot either side of it is identical, and it is one of only two verbs that work on a phone that is not casting.
app_close force-closes the app in front, the way a person does it: it opens the App Switcher and flicks that app's card up off the screen. Open the app you mean first, then call it; it closes whatever is in front and does not choose, so do not send it from a screen you have not read. It takes no fields at all, and is the one verb here that refuses a body carrying any: {"type":"app_close","url":"instagram://"} comes back as invalid_action rather than being read as "close whatever is in front" and answered with a 200 for the wrong app. It is pointer-driven and does not use the on-device helper, so it works on any casting phone even where that helper is not answering, which is the point of it.
You can do the same by hand with press appswitcher and then a swipe from the card up past the top of the screen, but the card you have to hit is not where most people assume: with more than one app open the front app's card is at the right of the switcher and the middle holds the app *before* it, so a flick up the centre line force-closes a bystander. app_close is that sequence done correctly. Both the switcher and lock ride a spare mouse button iOS binds to the system action rather than a keyboard combination, so they no longer depend on Full Keyboard Access being on, which it is not across the fleet.
It takes a few seconds and it checks its own work: it reads the screen again before answering, so a 200 means the app that was in front is no longer the one showing. How firm that is depends on the app. The check is a comparison of the whole frame, so an app that is animating anyway - a video, an autoplaying feed, a spinner - satisfies it whether or not its card went, and on those a 200 is worth about what a gesture's is. On a still screen it is the real thing. Read the screen back with GET /devices/{id}/snapshot when the close matters and the app was moving. It still carries effect_observed: false like every other gesture, because "no longer in front" is what can be seen and "terminated" is not. It is refused rather than reported as closed in two cases, both arriving as command_rejected: a phone whose App Switcher has never been bound, where nothing was touched, and a close that ran and left the same app on screen. Neither is a retry, whatever the generic detail on that answer says: with more than one app open the first flick may already have thrown a different app's card, and sending it again throws another. Read the screen back with GET /devices/{id}/snapshot instead.
recast re-establishes dropped screen mirroring and is the fix for a phone that is online but not casting. It is expensive: on a phone that is already casting the session is bounced, and the screen is gone while it reconnects. Budget up to two minutes and poll casting rather than assuming a fixed wait; 15 to 30 seconds is the good case and 75 seconds has been measured on a healthy phone. Never call it speculatively.
It answers as soon as the request has been accepted, so a 200 here says the reconnect *started*. Poll GET /devices/{id} until casting is true before driving the phone, and treat a phone still dark after two minutes as needing a look rather than another recast.
Retrying safely. Send an Idempotency-Key header, any string up to 255 characters, unique per action you intend to perform. A retry carrying the same key replays the first response instead of tapping the phone again, and comes back with Idempotent-Replay: true. Reusing a key for a different body is refused with idempotency_key_reused rather than answered from the cache, and a retry that arrives while the first is still running gets idempotency_in_flight. Keys are honoured for 24 hours and scoped to your API key.
times sends a press more than once inside one request. If the sequence stops early, because a press failed or because the request ran out of time, repeated reports how many actually landed and only those are charged. On a failure the same count is in the error detail, so a retry can pick up where it stopped.
Chaque action est un objet, choisi par type. * indique un champ obligatoire.