Desarrolladores

Referencia de la API

Controla tus teléfonos desde tu propio código y gestiona la agencia a su alrededor.

URL base
https://beta.aliremote.com/api/v1
Para modelos de lenguaje
/llms-full.txt

Autenticación

Cada petición lleva una clave API. Créala en el panel, en Acceso API, limitada a una agencia y a un conjunto de permisos y teléfonos. El secreto se muestra una sola vez.

Authorization: Bearer ali_live_<key_id>_<secret>

Un endpoint responde 403 scope_required si a la clave le falta el permiso, y 403 forbidden si quien la creó tampoco podría hacerlo en el panel.

  • devices.read
  • devices.snapshot
  • devices.write
  • devices.control
  • media.write
  • devices.stream
  • analytics.read
  • members.read
  • members.write
  • agency.read
  • groups.write
  • automations.read
  • automations.write

Convenciones

Coordinates
For every positional action, x/y are in the pixel space of the snapshot from GET /devices/{id}/snapshot. Read a position off that image and send it as-is; the API scales it to the device. Do not pre-scale.
Device ids
A device is addressed by its opaque public_id (ph_…). Paths also accept the sticker on the back of the handset (inventory_id) if your team labels racks that way.
Errors
Failures are RFC 7807 problem documents with a stable code. Integrate against code, never against detail, which is prose and may be reworded. Every response carries an X-Request-Id header, successes included; quote it in support tickets. Problem documents repeat it in the body as request_id, which is the only reason it looks error-only: a successful call has no body to put it in, so read the header. The call you most want to ask about is usually the one that returned 200.
What a key may do, beyond its scopes
A key acts on behalf of whoever created it, and can never do something that person could not do in the dashboard by hand: not read a team they cannot see, not hand out a role above their own standing, not reach another agency. Scopes narrow that further, they do not widen it. So a call can be refused 403 forbidden even when the scope is right, and the message names the account and the permission it is missing: give that person a role carrying it, or mint the key under somebody who already has one. This only affects the endpoints that read or change the agency (analytics, people, invitations, groups); the device endpoints depend on which phones the key reaches, which GET /usage reports as reachable_devices. That count is the budget multiplier and reads higher than GET /devices lists, because it still counts a handset no relay reports any more. If the account is deleted or moves agency, those agency endpoints answer 403 key_orphaned and the key needs replacing, while its device endpoints keep working.
Rate limits
Limits are token buckets, so each one has a sustained rate *and* a burst: 20/s per key (burst 120), 8/s per device for actions (burst 16), 2/s per device for snapshots (burst 4). A full bucket absorbs the burst before the sustained rate applies, so 4 snapshots fired at once all succeed and the next is refused. Refusals carry Retry-After and X-RateLimit-*, and those headers report whichever layer is closest to refusing you, not always the per-key one. A phone is physical hardware moving at its own pace; exceeding these does not make it faster.
Daily credits
Separate from the rate limits. Every call spends credits, priced by what it does: a read costs 1, an action 2, a screenshot 10, an upload 50, and a minute of streaming 20. One unit is one call, except where a call does more than one thing: press with times: 20 pays for twenty actions, and a stream pays per minute it stays open. Three layers pay for that, and they are not the same thing. Included: 6,000 credits per reachable phone plus a flat 2,500, free, and reset at UTC midnight. Purchased: credits bought in the dashboard, held by the agency rather than by one key and never reset. Only the part of a day above the included allowance draws on them, and a call neither layer can cover is refused 402 credits_exhausted: top up, or wait for the reset. The ceiling: 35,000 credits per reachable phone plus the same flat part, refused as 429 quota_exceeded whether or not there is a balance to pay with, so a loop that goes wrong costs one day rather than a balance. The pool is per key, not per phone. The phone count sizes it and nothing else, so a key reaching 40 handsets can spend its whole day on one of them; nothing is reserved for a phone and nothing is lost by leaving one idle. A key reaching fewer than 3 phones is sized as though it reached 3, because small accounts would otherwise be throttled out of ordinary use, and the flat part is what a key reaching no phones has to spend, so the agency endpoints keep answering when a key's scope collapses. A call that fails without doing any work is refunded to whichever layer paid for it, and GET /usage is free. These are the standard numbers and your account may have been given different ones, so read what your key actually has from GET /usage rather than hardcoding the figures above: it reports the allowance, the balance, and what today has taken from each. It reports the per-phone breakdown too, so it asks for devices.read: give a management-only key that scope if you want it to read its own budget.

Devices

GET/devices

List your devices

Permiso: devices.read

Every phone this key can reach. Paginated: pass next_cursor back as ?cursor= until it comes back null. A phone no relay reports any more is left out, so this page is the list the dashboard shows rather than a count of everything the agency is billed for: GET /agency reports that one as phones_held, and reachable_devices on GET /usage still sizes your budget by it. GET /devices/{id} still reads one, so an id you already hold resolves, but every action on it answers device_unprovisioned until a relay reports the handset again.

ParámetroEnTipoDescripción
cursorquerystringThe next_cursor from the previous page.
limitqueryinteger (min 1, max 200, default 50)

Devuelve DevicePage

EstadoSignificado
200A page of devices.
401unauthorized: Missing or invalid API key
403forbidden: Not permitted; scope_required: The key lacks a required scope
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/devices" -H "Authorization: Bearer $ALI_API_KEY"

GET/devices/{id}

One device

Permiso: devices.read

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).

Devuelve Device

EstadoSignificado
200The device.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

PATCH/devices/{id}

Rename a device, or set its inventory sticker

Permiso: devices.write

The two labels a customer owns: what the phone is called, and the sticker on the back of the handset. The sticker is unique across the fleet, which is what lets it be used in place of a public_id anywhere a device is named. null clears it, and a clash is refused rather than quietly moved off the phone that had it. Needs devices.write and phones.edit on the account that created the key, which is the same pair the dashboard asks for before it shows the field. A key that drives handsets all day cannot rename them.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.

Devuelve Device

EstadoSignificado
200The device as it now reads.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404device_not_found: No such device
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X PATCH "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

Seeing the screen

GET/devices/{id}/snapshot

Capture a screenshot

Permiso: devices.snapshot

The current screen as a JPEG. Coordinates you read off this image are the ones to send to /actions; the API scales them to the device. Rate limited harder than actions: each capture costs the box a full frame and a transcode. For continuous frames, stream instead of polling.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
EstadoSignificado
200A JPEG screenshot.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
409device_unprovisioned: Device is not connected to a box
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
503device_offline: Device unreachable
504kernel_timeout: Device did not respond in time
curl "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/snapshot" -H "Authorization: Bearer $ALI_API_KEY" --output screen.jpg

GET/devices/{id}/stream

Live screen (WebSocket)

Permiso: devices.stream

Connect with wss://. The server sends a JSON ready frame, then the screen as binary JPEG frames, one per message.

→ {"type":"ready","device":"ph_…","screen":{"width":405,"height":720},"max_fps":2}
→ <binary JPEG>  …
→ {"type":"closing","reason":"max_duration","reconnect":true}
← {"type":"ping"}   → {"type":"pong","t":…}
← {"type":"ack"}    optional; lets the device pace to what you actually render

Frame rate is capped at 5/s, default 2. This is not the rate the ALI console runs at: a person driving a pointer needs every frame, an integration checking a screen between actions does not, and the frames are relayed through us rather than served from the device directly. Reconnect. Connections are closed at a duration ceiling and on any upstream interruption. A closing frame with reconnect: true precedes an expected close; reconnect with backoff. Limits. 10 concurrent streams per key, and every minute open costs 20 credits from the daily pool, charged a minute at a time and reported under stream_minutes in GET /usage. A device that is not casting is refused: send a recast action first.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
fpsqueryinteger (min 1, max 5, default 2)Frames per second. Clamped to 5, never rejected.
tokenquerystringYour API key, for clients that cannot set headers on a WebSocket handshake (browsers). Prefer the Authorization header: query strings end up in logs.
EstadoSignificado
101Switching protocols. Frames follow.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
# WebSocket. Frames arrive as binary JPEG messages, one per frame.
wscat -c "wss://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/stream?fps=2" \
  -H "Authorization: Bearer $ALI_API_KEY"

Controlling a phone

POST/devices/{id}/ip-rotation

Rotate the device's public IP

Permiso: devices.control

A fresh public address, scored. Mobile data: an airplane-mode cycle. Proxy Wi-Fi: the router's link, which rotates every device on it, unscored. ALI Wi-Fi: 409 rotation_unavailable. Answers 202; poll GET on this path until status leaves running. One rotation per device at a time.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.

Devuelve IpRotation

EstadoSignificado
202The rotation has started. Poll GET for the outcome.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted
404device_not_found: No such device
409device_unprovisioned: Device is not connected to a box; rotation_unavailable: This device cannot rotate its IP; rotation_in_progress: An IP rotation is already running; idempotency_in_flight: The first request with this Idempotency-Key is still running
422idempotency_key_reused: This Idempotency-Key was used for a different request
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/ip-rotation" -H "Authorization: Bearer $ALI_API_KEY"

GET/devices/{id}/ip-rotation

The device's IP rotations

Permiso: devices.read

This device's rotations, newest first.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
EstadoSignificado
200Newest first.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/ip-rotation" -H "Authorization: Bearer $ALI_API_KEY"

POST/devices/{id}/actions

Send an action to a device

Permiso: devices.control

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.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.

Devuelve ActionResult

Cada acción es un objeto, elegido por type. * marca un campo obligatorio.

TipoCampos
tapx* integer (min 0, max 10000), y* integer (min 0, max 10000)
long_pressx* integer (min 0, max 10000), y* integer (min 0, max 10000), hold_ms integer (min 0, max 10000, default 600)
swipex1* integer (min 0, max 10000), y1* integer (min 0, max 10000), x2* integer (min 0, max 10000), y2* integer (min 0, max 10000), precise boolean, duration_ms number, steps number, step_ms number, stepping number, step_sleep number, brake boolean
dragx1* integer (min 0, max 10000), y1* integer (min 0, max 10000), x2* integer (min 0, max 10000), y2* integer (min 0, max 10000), precise boolean, duration_ms number, steps number, step_ms number, stepping number, step_sleep number, brake boolean
swipe_directiondirection* up | down | left | right, distance , span number, precise boolean, duration_ms number, steps number, step_ms number, stepping number, step_sleep number, brake boolean
scrollx* integer (min 0, max 10000), y* integer (min 0, max 10000), dy* integer (min -10000, max 10000), precise boolean
texttext* string (min length 1, max length 4096)
pressname* home | appswitcher | lock | screenshot | enter | keyboard | controlcenter | notifications | volumeup | volumedown | mute | backspace | tab | up | down | left | right, times integer (min 1, max 20, default 1)
open_urlurl* string (min length 1, max length 2048)
clipboard_settext* string (max length 4096)
clipboard_getnone
airplaneon* boolean
device_ippublic boolean
wifion* boolean
cellularon* boolean
torchon* boolean
brightnesslevel* number (min 0, max 1)
recastnone
calibratenone
app_closenone
send_binding_keynone
EstadoSignificado
200Performed, and the device confirmed it (device_ack: true). Most actions return only that; clipboard_get adds { clipboard } and device_ip the address.
202Handed to the box, outcome unconfirmed (device_ack: false). The box hosting this device is on a build that cannot report the result. Verify with a snapshot.
400invalid_action: The action failed validation
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted
404device_not_found: No such device
409device_unprovisioned: Device is not connected to a box; device_not_provisioned_for_action: The phone's on-device helper did not answer in time; device_in_use: Device in use; idempotency_in_flight: The first request with this Idempotency-Key is still running
422command_rejected: The device rejected the command; idempotency_key_reused: This Idempotency-Key was used for a different request
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
503device_offline: Device unreachable
504kernel_timeout: Device did not respond in time
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/actions" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"tap","x":229,"y":400}'

GET/devices/{id}/control-session

See who is driving a device

Permiso: devices.read

Whether the phone is free, held by a person, or held by an automation, and since when. The read that makes the rest of this section usable. Without it, the only way to learn a phone was busy was to be refused mid-run, which is late: a scheduler wants to skip a busy handset and come back to it, not fail a step halfway through a sequence and unwind what it already did. yours is true when this key is the holder, so a caller can tell somebody-else-has-it from I-have-it without comparing names.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
EstadoSignificado
200Who holds it now.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/control-session" -H "Authorization: Bearer $ALI_API_KEY"

POST/devices/{id}/control-session

Declare that an automation is driving a device

Permiso: devices.control

Tells the dashboard a script has this phone, and what to call it. Anyone who opens the device sees the label and starts as a spectator. Every control call already takes a lease lasting about 30 seconds, so an integration that never calls this is still protected from somebody typing over it. Declare a session for the two things that lease cannot do: give the automation a name a person can act on, and hold the phone through the gaps when the script is thinking rather than tapping. It is not a lock. A person can take the phone deliberately, from a dialog naming this session, and the next control call then answers device_in_use, which is how the automation learns to stop instead of tapping into somebody's work. Release it when done; it also expires on its own.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).

Acepta ControlSessionRequest

EstadoSignificado
200The phone is yours until expires_at, or until a person takes it.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
409device_in_use: Device in use
429rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/control-session" -H "Authorization: Bearer $ALI_API_KEY"

DELETE/devices/{id}/control-session

End an automation's session

Permiso: devices.control

Lets people back on immediately rather than when the lease lapses. A no-op if somebody has already taken the phone.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
EstadoSignificado
200Released.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
429rate_limited: Too many requests
curl -X DELETE "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/control-session" -H "Authorization: Bearer $ALI_API_KEY"

POST/devices/{id}/control-session/takeover

Take a device somebody else is driving

Permiso: devices.control

Take a phone that somebody else is currently driving. Control of a phone is a lease: one holder at a time, everybody else watches. Until this endpoint existed an automation that found a phone busy could only give up, and the commonest reason a phone is busy is a console somebody left open on a second monitor, which nothing will ever clear. What happens depends on who is holding it. - Nobody, or this same key. Taken straight away. - Another automation. Taken straight away. This mirrors the rule the dashboard already applies in the other direction, where a person may take a phone off a script without asking. - A person. They are asked. Their console shows the request and they have about three seconds to refuse; silence is a yes, which is what stops an open tab holding a phone all afternoon. This request blocks for those three seconds, so allow for it in your client timeout. - The account this key belongs to. Taken straight away, and nobody is asked. A credential is not somebody else, and asking would leave an automation stuck behind its own owner's open tab with no way through. This is the one case where asked is false and taken_from.kind is person. A refusal answers 409 device_in_use, and asking again immediately is refused for longer each time: five seconds, then fifteen, then a minute. The four refusals share that one code and are told apart by detail: a person who said no, a cooldown still running, somebody else already mid-ask, and an ask that did not settle. Treat any of them as a phone somebody needs and move on to another one. Who does the asking. The account that created the API key, so the dialog names a colleague rather than a credential nobody recognises. If that account has left the agency the request answers key_orphaned: mint a replacement key under somebody who is still there. It is not a lock in the other direction either. Whoever was taken off keeps watching and can take the phone straight back, at which point your next control call answers device_in_use. That is the signal to stop rather than to retry. Call GET /devices/{id}/control-session first if you would rather skip a busy phone than interrupt one.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).

Acepta ControlTakeoverRequest

EstadoSignificado
200The device is yours until expires_at. taken_from says who was on it, and asked whether a person had to be asked.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
404device_not_found: No such device
409device_in_use: Device in use
429rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/control-session/takeover" -H "Authorization: Bearer $ALI_API_KEY"

Sending files

POST/devices/{id}/media

Start sending a file to a device

Permiso: media.write

Uploading is three steps, and the bytes never pass through the API: 1. POST /devices/{id}/media with the file's filename, mime and size_bytes201 with an upload_url. 2. PUT the raw bytes to upload_url before upload_expires_at (curl -X PUT --data-binary @photo.jpg "$upload_url"). If you set a Content-Type, it must be the one you declared. 3. POST /devices/{id}/media/{asset_id}/complete202. Storage is checked for what actually arrived, and the delivery to the phone is queued. The upload URL points directly at storage, which is what makes the 100 MB limit real: a body routed through the API itself would be capped at 4.5 MB by the platform, regardless of what this document promised. Delivery then runs on a queue: putting a file onto a physical handset takes minutes rather than milliseconds. Poll GET /devices/{id}/media/{asset_id} for the outcome, and read error_code beside error when it ends failed if you want something stable to branch on.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
Idempotency-Keyheaderstring (max length 255)Unique per upload you intend to start. A retry with the same key replays the first response and answers with Idempotent-Replay: true. Honoured for 15 minutes here rather than 24 hours, because that is how long the upload_url it replays stays valid; after that the same key starts a fresh upload.

Acepta UploadRequest · Devuelve UploadTicket

EstadoSignificado
201A slot is reserved. PUT the bytes to upload_url before it expires, then call /complete.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device
409device_unprovisioned: Device is not connected to a box
413payload_too_large: Payload too large
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
# 1. describe the file; a presigned URL comes back
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/media" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"photo.jpg","mime":"image/jpeg","size_bytes":248000}'

# 2. PUT the bytes straight to that URL
curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --data-binary @photo.jpg

# 3. confirm, which queues delivery to the phone
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/media/$ASSET_ID/complete" -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" -d '{}'

POST/devices/{id}/media/{asset_id}/complete

Finish an upload and queue the delivery

Permiso: media.write

Call after PUTting the bytes to upload_url. The vault verifies what actually arrived: size and type against what was declared, then queues the delivery to the phone. Answers 202; poll GET /devices/{id}/media/{asset_id} for the outcome. Calling it again re-queues the delivery, so it doubles as the retry.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
asset_id *pathstring
albumquerystringAlbum to file it under on the phone.

Devuelve UploadAccepted

EstadoSignificado
202Verified and queued.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device; not_found: Not found
409device_unprovisioned: Device is not connected to a box; device_in_use: Device in use
429rate_limited: Too many requests
502upload_failed: Upload could not be completed
curl -X POST "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/media/ASSET_ID/complete" -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" -d '{}'

DELETE/devices/{id}/media/{asset_id}

Take a file back off a device

Permiso: media.write

Remove a file this API put on the phone. Asynchronous, for the same reason the upload is: the work happens on the handset itself. Answers 202 with removing; poll GET on this path until removed. A removal is confirmed by reading the phone's album back afterwards, never by trusting the result of the delete itself. A removal that cannot be confirmed is reported as still delivered rather than quietly marked clean. Removing something already gone answers 404 not_found, which makes a repeat safe. A removal asked for while the file is still on its way to the phone answers command_rejected: wait for the delivery to settle first. One-time consent. The first removal on a handset can hang and fail until somebody answers Apple's "Delete Always" prompt on that phone once. Until then the file stays put, and the error says so rather than reporting a clean removal.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
asset_id *pathstring
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.
EstadoSignificado
202Removal queued. Poll GET on this same path until removed.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404device_not_found: No such device; not_found: Not found
409device_unprovisioned: Device is not connected to a box
422command_rejected: The device rejected the command
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X DELETE "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/media/ASSET_ID" -H "Authorization: Bearer $ALI_API_KEY"

GET/devices/{id}/media/{asset_id}

Whether an upload landed

Permiso: devices.read

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).
asset_id *pathstring

Devuelve Delivery

EstadoSignificado
200The delivery.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/media/ASSET_ID" -H "Authorization: Bearer $ALI_API_KEY"

Automations

GET/automations

List automations

Permiso: automations.read

An automation is a flow of steps that drives phones on a schedule, on an event, or when you start one. Until now these scopes had nothing behind them over HTTP: the feature was reachable from the dashboard canvas and from an assistant, and a customer reading automations.read in the scope list was being shown a promise this API did not keep. These endpoints are the same store both of those call, so a flow built here is the flow the canvas draws and the scheduler runs. How a flow is built. 1. POST /automations for an empty one. It starts turned off and unpublished, so creating it can never start anything. 2. GET /automation-blocks for the steps that exist, with their settings and the ports their edges leave by. Do not guess these. 3. PATCH /automations/{id} with the whole graph. A draft is saved even when it is not yet valid, and the answer lists every problem at once rather than the first, so a flow can be built over several calls. 4. POST /automations/{id}/publish to make the draft the version that runs. This is the step that refuses a broken flow, which is what keeps a half-built automation off a schedule. 5. POST /automations/{id}/triggers to say when it should run, then PATCH with enabled: true. What a run reaches. A run acts with the standing of the account that created the API key, so it drives the phones that person drives and no others. Everything it does spends the same credits the same actions would over /devices. Watching one. Starting a run answers 202 and a run_id; it does not finish while you wait, because it drives physical phones. GET /automation-runs/{id} reports every step in order, which phone each acted on, and why any was skipped.

EstadoSignificado
200Every automation in the agency, with whether it is published, whether it is on, and how its last run went.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automations" -H "Authorization: Bearer $ALI_API_KEY"

POST/automations

Create an automation

Permiso: automations.write

Creates an empty automation with a name. It starts turned off and unpublished, so nothing runs until you save steps with PATCH and then publish it. name is capped at 120 characters and description at 2000.

ParámetroEnTipoDescripción
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.
EstadoSignificado
201Created, turned off and unpublished.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/automations" -H "Authorization: Bearer $ALI_API_KEY"

GET/automations/{id}

Get one automation

Permiso: automations.read

Its published steps, its unpublished draft if there is one, its triggers and its version history. Read this before changing a flow, so you edit what is actually there.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
EstadoSignificado
200The automation.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

PATCH/automations/{id}

Rename, switch on, or save a new draft

Permiso: automations.write

graph replaces the draft rather than merging into it, so send the whole flow; read GET /automation-blocks first. A draft is saved even when it is not yet valid. The answer carries draft.problems, every problem at once rather than the first, and publishable. Nothing saved here runs until you publish it. enabled: true is refused until something has been published, because a switch that reports on and does nothing is worse than a refusal.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
EstadoSignificado
200Saved. draft.problems is empty when it is ready to publish.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X PATCH "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

DELETE/automations/{id}

Delete an automation

Permiso: automations.write

Deletes it and everything it recorded: versions, triggers and run history. This cannot be undone. To stop it running, PATCH with enabled: false instead.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
EstadoSignificado
200Gone.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X DELETE "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

POST/automations/{id}/publish

Publish the current draft

Permiso: automations.write

Makes the draft the version that runs. This is the step that checks the whole flow and refuses if anything is wrong, so a broken automation can never end up on a schedule. Runs already in progress keep the version they started with.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.
EstadoSignificado
200Published. This version is what triggers now run.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26/publish" -H "Authorization: Bearer $ALI_API_KEY"

GET/automations/{id}/runs

List this automation's runs

Permiso: automations.read

Newest first, with how each one ended.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
EstadoSignificado
200Recent runs.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26/runs" -H "Authorization: Bearer $ALI_API_KEY"

POST/automations/{id}/runs

Start a run now

Permiso: automations.write

Answers 202 with a run_id as soon as the run is created. It does not finish while you wait, because it drives physical phones: poll GET /automation-runs/{id}. The run acts with the standing of the account behind the key, so it reaches the phones that person reaches and no others.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.
EstadoSignificado
202Started. Poll the run for how it goes.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26/runs" -H "Authorization: Bearer $ALI_API_KEY"

POST/automations/{id}/triggers

Add a trigger

Permiso: automations.write

manual for by hand only, schedule with a five-field cron such as 0 2 * * * read in the agency's timezone unless you name one, or event with an event_type for when a phone changes state. An automation with no trigger can still be started with POST /automations/{id}/runs.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.
EstadoSignificado
201Added.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26/triggers" -H "Authorization: Bearer $ALI_API_KEY"

DELETE/automations/{id}/triggers/{trigger_id}

Remove a trigger

Permiso: automations.write

Stops the automation being started that way. Its other triggers are left alone.

ParámetroEnTipoDescripción
id *pathstring (uuid)The automation's id, from GET /automations.
trigger_id *pathstring (uuid)The trigger's id, from GET /automations/{id}.
EstadoSignificado
200Removed.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X DELETE "https://beta.aliremote.com/api/v1/automations/ph_0b491ebdeb254769ab26/triggers/{trigger_id}" -H "Authorization: Bearer $ALI_API_KEY"

GET/automation-blocks

List the steps a flow can be built from

Permiso: automations.read

Every kind of step, with what it does, what settings it takes and which ports its edges may leave by. Read this before writing a graph: it is the only way to know what exists, and it is filtered to what this key's account is allowed to use, so a step listed here is one you may actually publish.

EstadoSignificado
200The catalogue, and one paragraph on how a flow is wired.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automation-blocks" -H "Authorization: Bearer $ALI_API_KEY"

GET/automation-runs

List recent runs

Permiso: automations.read

Across every automation, newest first. The one call that answers whether last night worked.

ParámetroEnTipoDescripción
automation_idquerystring (uuid)Only this automation's runs.
limitqueryinteger (min 1, max 200)Defaults to 50.
EstadoSignificado
200Recent runs.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automation-runs" -H "Authorization: Bearer $ALI_API_KEY"

GET/automation-runs/{id}

Get one run, step by step

Permiso: automations.read

Every step in order, which phone each acted on, and why any was skipped or failed. This is what to read when somebody asks why an automation did not do what they expected.

ParámetroEnTipoDescripción
id *pathstring (uuid)The run's id.
EstadoSignificado
200The run and its steps.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automation-runs/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

POST/automation-runs/{id}/cancel

Stop a run

Permiso: automations.write

Stops a run that is still going. It stops at its next step rather than mid-action, so a phone is never left half way through something. A run that has already finished answers not_found rather than pretending to stop.

ParámetroEnTipoDescripción
id *pathstring (uuid)The run's id.
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.
EstadoSignificado
200It will stop at its next step.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/automation-runs/ph_0b491ebdeb254769ab26/cancel" -H "Authorization: Bearer $ALI_API_KEY"

Organising your rack

GET/groups

List your groups

Permiso: devices.read

Groups are your own filing of your own phones: a label, and the phones in it. There is no create, rename or delete endpoint, and that is the product rather than a gap. A group is created by naming it on a phone (new_group_name) and swept away when the last phone leaves it, which is what stops an agency accumulating empty groups somebody made once. The dashboard works the same way. Counts are of every phone in the group, including ones this key cannot reach.

EstadoSignificado
200Every group in the agency.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/groups" -H "Authorization: Bearer $ALI_API_KEY"

PUT/devices/{id}/groups

Set the groups a device is in

Permiso: groups.write

The complete set, never a delta: whatever you send is what the phone ends up in, so a client working from a stale read cannot silently drop a group it never knew about. {"group_ids": []} takes it out of everything. new_group_name is the only way to create a group. A group left with no phones stops existing, which is how they are cleaned up. There is no delete. Answers with the agency's whole group list, because one write can both create a group and empty another away, so every count may have moved.

ParámetroEnTipoDescripción
id *pathstringThe device's public_id, or its sticker (inventory_id).

Acepta SetDeviceGroups

EstadoSignificado
200The agency's groups, after the change.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404device_not_found: No such device
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
# The complete set. An empty array files the phone under nothing.
curl -X PUT "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/groups" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"group_ids":[],"new_group_name":"Night shift"}'

Analytics

GET/analytics/summary

Agency totals and the daily trend

Permiso: analytics.read

What the agency did over the period: worked minutes, how many people and phones were active, gestures sent, and a point per day. Figures come from the same rollups the dashboard reads, so the two always agree. Days are drawn on the agency's timezone rather than UTC (period.timezone says which), and the retention window bounds how far back days can reach.

ParámetroEnTipoDescripción
daysqueryinteger (min 1, max 365, default 7)How many days back to report, ending today. Refused rather than clamped if out of range: a range is the meaning of every number in the response, so silently answering for 365 days when you asked for more would be wrong in a way nothing in the payload reveals.

Devuelve AnalyticsSummary

EstadoSignificado
200Totals and trend.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/analytics/summary?days=30" -H "Authorization: Bearer $ALI_API_KEY"

GET/analytics/members

What each person did

Permiso: analytics.read

Per-person desk minutes, worked minutes, phones touched, sittings and gestures, with the last time each was active. presence_minutes is desk time, with overlaps merged so six phones driven at once is one minute; active_minutes is the part of it that carried a gesture. An agency paying by the hour and an agency measuring effort want different ones, and neither can be derived from the other. untrusted_minutes is the part of active_minutes that arrived too late to corroborate against a second signal. It is included in the total, and reported separately so a payroll integration knows which part is soft. ?include=devices adds a devices array to each member: which phones their minutes were spent on, from a daily rollup kept for years. Off by default because it multiplies the response by the rack rather than by the team. That half is limited to the phones this key reaches, since it names handsets, and its active_minutes are phone-minutes: they legitimately sum to more than the member's own, because driving two handsets in one minute is one minute worked and two phones used. For the times rather than the totals, read GET /analytics/sessions. Figures come from the same rollups the dashboard reads, so the two always agree. Days are drawn on the agency's timezone rather than UTC (period.timezone says which), and the retention window bounds how far back days can reach.

ParámetroEnTipoDescripción
daysqueryinteger (min 1, max 365, default 7)How many days back to report, ending today. Refused rather than clamped if out of range: a range is the meaning of every number in the response, so silently answering for 365 days when you asked for more would be wrong in a way nothing in the payload reveals.
includequery`devices`Comma-separated optional sections. devices is the only one today. An unknown name is refused rather than ignored, so a typo reads as an error instead of as a missing feature.

Devuelve MemberUsageReport

EstadoSignificado
200One row per member.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/analytics/members?days=30" -H "Authorization: Bearer $ALI_API_KEY"

GET/analytics/devices

What each phone carried, and which carried nothing

Permiso: analytics.read

Per-phone activity, plus untouched, the phones you hold that nobody used in the period. That is the line item that costs money. Limited to the phones this key reaches, unlike the other analytics endpoints. Those report agency totals and name no phone; this one is an inventory listing with numbers attached, so a key scoped to three handsets sees three. Figures come from the same rollups the dashboard reads, so the two always agree. Days are drawn on the agency's timezone rather than UTC (period.timezone says which), and the retention window bounds how far back days can reach.

ParámetroEnTipoDescripción
daysqueryinteger (min 1, max 365, default 7)How many days back to report, ending today. Refused rather than clamped if out of range: a range is the meaning of every number in the response, so silently answering for 365 days when you asked for more would be wrong in a way nothing in the payload reveals.

Devuelve DeviceUsageReport

EstadoSignificado
200One row per reachable phone.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/analytics/devices?days=30" -H "Authorization: Bearer $ALI_API_KEY"

GET/analytics/sessions

Who was on which phone, and when

Permiso: analytics.read

One row per person per phone per stretch: username was on device_name from started_at to ended_at. The grain the other reports aggregate away, published raw so you can roll it up whichever way your business asks. This is the report to reach for when something has gone wrong on one handset and the question is who was driving it. /analytics/members says somebody touched ten phones and /analytics/devices says a phone carried an hour; neither says which ten, or whose hour. Limited to the phones this key reaches, like GET /analytics/devices and unlike the agency totals: it names handsets. Paginated, newest first. A sitting is written every time somebody opens a phone, so a busy agency produces thousands a week. Pass next_cursor back as ?cursor= until it comes back null. Two retention lines, and they are different. Sittings themselves are kept for retention_days. The per-minute grain that active_minutes and gestures are computed from is kept for 8 days, so a sitting that started before activity_from reports both as null rather than as zero. For totals older than that, read GET /analytics/members?include=devices, which comes from a daily rollup kept for years. `active_minutes` can exceed `minutes` on a short sitting, and that is not a bug: the minute is the grain, so a stretch from 09:29:50 to 09:30:10 touches two buckets and rounds to zero wall-clock minutes. A minute shared by two sittings is counted once, against the later one, so the sessions never sum to more than the day. A sitting by ALI support carries support: true with a null member_id and username. That somebody from ALI was on your phone is yours to know; which of us it was is not. Figures come from the same rollups the dashboard reads, so the two always agree. Days are drawn on the agency's timezone rather than UTC (period.timezone says which), and the retention window bounds how far back days can reach.

ParámetroEnTipoDescripción
daysqueryinteger (min 1, max 365, default 7)How many days back to report, ending today. Refused rather than clamped if out of range: a range is the meaning of every number in the response, so silently answering for 365 days when you asked for more would be wrong in a way nothing in the payload reveals.
cursorquerystringThe next_cursor from the previous page.
limitqueryinteger (min 1, max 500, default 100)Clamped rather than refused, unlike days: a page size is a hint and serving fewer rows costs you one more round trip, while a range changes what every figure in the body means.

Devuelve SessionPage

EstadoSignificado
200A page of sittings.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/analytics/sessions?days=30" -H "Authorization: Bearer $ALI_API_KEY"

GET/analytics/activity

Worked minutes by hour of the week

Permiso: analytics.read

cells[day][hour], Monday first, always 7 rows of 24, so a quiet hour is a zero rather than a missing entry and you can index it without bounds checks. peak is the busiest cell, for scaling a colour ramp in one pass. The week is the agency's local one. Figures come from the same rollups the dashboard reads, so the two always agree. Days are drawn on the agency's timezone rather than UTC (period.timezone says which), and the retention window bounds how far back days can reach.

ParámetroEnTipoDescripción
daysqueryinteger (min 1, max 365, default 7)How many days back to report, ending today. Refused rather than clamped if out of range: a range is the meaning of every number in the response, so silently answering for 365 days when you asked for more would be wrong in a way nothing in the payload reveals.

Devuelve ActivityReport

EstadoSignificado
200A 7 × 24 grid of minutes.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/analytics/activity?days=30" -H "Authorization: Bearer $ALI_API_KEY"

People and roles

GET/members

List the people in your agency

Permiso: members.read

Everybody with an account in this agency, and the roles each of them holds. Unpaginated, unlike GET /devices: an agency's seat count is capped in the low tens by its plan, so there is no second page to fetch.

EstadoSignificado
200Every member.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/members" -H "Authorization: Bearer $ALI_API_KEY"

GET/members/{id}

One member

Permiso: members.read

ParámetroEnTipoDescripción
id *pathstring (uuid)The member's id, from GET /members.

Devuelve Member

EstadoSignificado
200The member.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/members/8f14e45f-ea3c-4b21-9e35-1d9a6c9b2f10" -H "Authorization: Bearer $ALI_API_KEY"

PATCH/members/{id}

Set which roles a member holds

Permiso: members.write

Set the complete list of roles somebody holds. Never a delta: whatever you send is what they end up with, so a client working from a stale read cannot silently drop a role it never knew about. An empty array leaves the account able to do nothing. The only write in this API that changes what a person can do, and three things bound it: - A key never outranks its creator. It acts as whoever minted it, so it can only hand out a role that person could hand out by hand: at or below their own standing, carrying nothing they do not already hold. Read assignable on GET /roles. - It cannot act on somebody at or above that standing. A member who outranks the key's creator, a peer of theirs, and the creator themselves all read back as 404: the same answer as an id that does not exist, because a refusal that explained itself would confirm which ids are senior accounts. - So an agency cannot be left without an owner through this endpoint, since taking the last owner's role away would mean acting on a peer or on yourself. Changing an owner is a dashboard operation. Nothing else about a member is writable here. Usernames, addresses and two-factor belong to the person, not to the agency, and are theirs to change.

ParámetroEnTipoDescripción
id *pathstring (uuid)The member's id, from GET /members.

Acepta SetMemberRoles · Devuelve Member

EstadoSignificado
200The member, with their new roles.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
# The complete set of roles. Ids come from GET /roles.
curl -X PATCH "https://beta.aliremote.com/api/v1/members/8f14e45f-ea3c-4b21-9e35-1d9a6c9b2f10" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"role_ids":["8f14e45f-ea3c-4b21-9e35-1d9a6c9b2f10"]}'

GET/roles

List the roles in your agency

Permiso: members.read

Every role, what it allows, and whether this key may hand it out. Read assignable before inviting somebody: a key carries the standing of whoever created it, so it can only grant a role that person could grant. Checking here is the difference between choosing a role you can use and finding out from a 403 after you have already picked an address. Read-only. Creating a role means choosing a set of permissions, which is a decision for a screen that explains what each one does.

EstadoSignificado
200Every role in the agency.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/roles" -H "Authorization: Bearer $ALI_API_KEY"

GET/invitations

List invitations

Permiso: members.read

Every invitation this agency has issued, including the settled ones: accepted, expired, withdrawn. "Did that invitation ever land" is the question this list answers, and one that hid everything but the live ones would answer it with silence.

EstadoSignificado
200Every invitation.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/invitations" -H "Authorization: Bearer $ALI_API_KEY"

POST/invitations

Invite somebody into your agency

Permiso: members.write

Invite somebody into your agency. The account is created when they accept, never here. This writes an invitation and sends a link, so no password is chosen, and no seat is taken until somebody takes it. The invitation names a role, from GET /roles. A key can only hand out a role its creator could, so check assignable before choosing one. An address that already signs in anywhere is refused with invalid_request: a person belongs to exactly one agency, and moving somebody between agencies is a separate, deliberate operation. So is a second live invitation to an address that already has one: withdraw or resend the first instead. The link expires. POST /invitations/{id}/resend issues a fresh one and invalidates the old. Retrying safely. Send an Idempotency-Key. It matters less here than on an action, because a duplicate invitation is already refused by a uniqueness rule; what it buys is that a client retrying a timeout gets the original 201 and the invitation back, rather than an error about a pending invitation whose id it cannot see.

ParámetroEnTipoDescripción
Idempotency-Keyheaderstring (max length 255)Unique per action you intend to perform. A retry with the same key replays the first response instead of repeating the work, and answers with Idempotent-Replay: true. Honoured for 24 hours.

Acepta CreateInvitation · Devuelve Invitation

EstadoSignificado
201Invited, and the mail is on its way.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
409idempotency_in_flight: The first request with this Idempotency-Key is still running
422idempotency_key_reused: This Idempotency-Key was used for a different request
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/invitations" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"newva@example.com","role_id":"8f14e45f-ea3c-4b21-9e35-1d9a6c9b2f10"}'

DELETE/invitations/{id}

Withdraw an invitation

Permiso: members.write

The link stops working; the record stays, because it happened and this list is a history as much as a queue. An invitation somebody already accepted cannot be withdrawn. They have an account now, and PATCH /members/{id} with an empty role set is the operation that means what withdrawing would have meant.

ParámetroEnTipoDescripción
id *pathstring (uuid)The invitation's id, from GET /invitations.
EstadoSignificado
200Withdrawn.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X DELETE "https://beta.aliremote.com/api/v1/invitations/8f14e45f-ea3c-4b21-9e35-1d9a6c9b2f10" -H "Authorization: Bearer $ALI_API_KEY"

POST/invitations/{id}/resend

Send the invitation again

Permiso: members.write

Mints a new token and restarts the clock, so the previous link stops working. That is deliberate: a link forwarded to the wrong address, or sitting in a mailbox somebody else now reads, is dead the moment you resend. Not idempotent. Each call is meant to produce a fresh link, and replaying a stored response would hand back one that had already been superseded.

ParámetroEnTipoDescripción
id *pathstring (uuid)The invitation's id, from GET /invitations.

Devuelve Invitation

EstadoSignificado
200Sent again, with a new link and a new expiry.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope; forbidden: Not permitted; key_orphaned: The account this key acts for is gone
404not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X POST "https://beta.aliremote.com/api/v1/invitations/8f14e45f-ea3c-4b21-9e35-1d9a6c9b2f10/resend" -H "Authorization: Bearer $ALI_API_KEY"

Your agency

GET/agency

The agency this key belongs to

Permiso: agency.read

What a provisioning script reads before it orders anything: phones_held against phone_limit answers "may I add another", and timezone is the clock every analytics figure is drawn on, so you can align your own day boundaries instead of guessing at UTC. Read-only. Renaming an agency, moving its timezone or turning on its two-factor requirement all change what every member sees, and the last changes whether they can sign in at all. Those stay in the dashboard.

Devuelve Agency

EstadoSignificado
200The agency.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/agency" -H "Authorization: Bearer $ALI_API_KEY"

Usage and events

GET/events

Change feed (WebSocket)

Permiso: devices.read

Connect with wss://. Pushes transitions, not state: use GET /devices for the current picture and this to learn what changed. Event types: device.online, device.offline, device.casting, device.not_casting, media.delivered, media.failed.

→ {"type":"ready","devices":12,"events":[…],"poll_interval_ms":5000}
→ {"type":"device.offline","device":"ph_…","at":"…"}
→ {"type":"media.delivered","device":"ph_…","asset_id":"…","at":"…"}
← {"type":"ping"}   → {"type":"pong","t":…}

The devices count on ready is what this socket is watching, which is every phone the key reaches. That is wider than GET /devices lists, deliberately: a handset dropping off its relay leaves the list and you are still told it went offline. Exists so you do not have to poll GET /devices on a timer, which costs you latency and both of us a full authenticated request per tick.

ParámetroEnTipoDescripción
tokenquerystringAs above; prefer the Authorization header.
EstadoSignificado
101Switching protocols. Events follow.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
429rate_limited: Too many requests
wscat -c "wss://beta.aliremote.com/api/v1/events" \
  -H "Authorization: Bearer $ALI_API_KEY"

GET/usage

Today's credits, allowance and balance

Permiso: devices.read

What the current UTC day has spent and what is left: the free allowance, the purchased balance, what each kind of work costs, and where the credits went, by kind and by phone. This call is free.

Devuelve Usage

EstadoSignificado
200Usage snapshot.
401unauthorized: Missing or invalid API key
403scope_required: The key lacks a required scope
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/usage" -H "Authorization: Bearer $ALI_API_KEY"

Esquemas

Los objetos que estos endpoints devuelven y aceptan. Un campo marcado con * siempre está presente.

Device

CampoTipoDescripción
public_id *stringOpaque id used in every device call. Paths also accept the sticker.
name *string
inventory_idstring | nullThe sticker on the back, if it has one.
status *online | offline
casting *booleanWhether the phone is mirroring to its box. A phone can be online and not casting, in which case a snapshot has nothing to capture and actions land on a black screen. Send a recast action.
screen{ width: integer, height: integer } | nullPixel space of the snapshot, and of action coordinates.
ios_versionstring | null
regionnullAlways null. Boxes no longer carry a region (removed 2026-08-20); the property is kept for one release so existing clients keep parsing, and will be dropped in the next.
last_seen_atstring | null (date-time)When the box hosting this phone last reported in. status and casting are both derived from it, so this is what lets you judge how fresh they are rather than trusting a boolean computed on our clock.
box{ version: string | null } | nullThe box hosting this phone. The fleet is mixed and the API behaves differently across it, so the build is published to let you group by it: a 202 on an action means the box cannot report outcomes, and /snapshot answers device_unprovisioned on a build without snapshot support.

DevicePage

CampoTipoDescripción
devices *Device[]
next_cursor *string | null

ControlSessionRequest

Declares an automation as the driver of a phone, so people who open it start as spectators and can see what is running.

CampoTipoDescripción
label *string (max length 64)What a person should be told is running. "Nightly poster", not "job-4417".
ttl_secondsinteger (min 10, max 3600)How long to hold the phone without another call. Defaults to the implicit lease length.

ControlTakeoverRequest

Takes a phone off whoever is currently driving it. The label is required rather than optional here: it is what the person losing the phone is shown, and "an API key" is not something they can act on.

CampoTipoDescripción
label *string (max length 64)What a person should be told is taking the phone. "Nightly poster", not "job-4417".
ttl_secondsinteger (min 10, max 3600)How long to hold the phone without another call. Defaults to the implicit lease length.

UploadRequest

CampoTipoDescripción
filename *string (max length 200)The name to store it under. No path separators.
mime *stringThe file's content type, e.g. image/jpeg or video/mp4.
size_bytes *integer (max 104857600)The exact size of the file. Verified against what actually arrives, with a small tolerance.

UploadTicket

CampoTipoDescripción
asset_id *string
upload_url *stringPresigned PUT. Send the raw bytes here; the file never passes through the API.
upload_expires_at *string (date-time)
status *awaiting_upload

UploadAccepted

CampoTipoDescripción
asset_id *string
status *queued

Delivery

CampoTipoDescripción
asset_id *string
filenamestring
status *awaiting_upload | stored | queued | sending | delivered | failedawaiting_upload and stored describe the upload before /complete has queued it; the rest come from the delivery queue.
attemptsinteger
errorstring | null
delivered_atstring | null (date-time)
updated_atstring (date-time)

Budget

Credits spent today, the ceiling they are spent against, and the gap.

CampoTipoDescripción
usedinteger
budgetinteger
remaininginteger

ActionResult

What came back from an action. Extra fields appear for the verbs that return a value.

CampoTipoDescripción
device_ack *booleanWhether the device confirmed it accepted the command. Not a claim that the screen changed, which nothing can observe for you, but it separates a confirmed hand-off from an unconfirmed one without inferring it from the status code.
effect_observedbooleanPresent on swipe, drag, swipe_direction and scroll, and always false. A gesture can be accepted in full and still move nothing, and unlike a tap there is no way to tell from here. Read the screen back with GET /devices/{id}/snapshot when it matters.
repeatedintegerPresent when times was used: how many presses were sent.
clipboardstringclipboard_get only.
ipstring | nulldevice_ip only.
publicbooleandevice_ip with public: true only: present when the address is the one the internet sees.

IpRotation

One rotation of one device's public address.

CampoTipoDescripción
id *string (uuid)
kind *sim | proxyHow it rotates.
status *running | rotated | unchanged | failedunchanged: the same address twice.
step *read_before | cut | restore | read_after | fraud_check | done
attempt *integer
ip_beforestring | null
ip_afterstring | null
fraud_scoreinteger | null0 clean to 100.
fraud_risklow | medium | high | critical | nullBands of 25.
failurenot_rotatable | phone_busy | phone_offline | helper_unreachable | box_too_old | no_address_after | proxy_not_configured | proxy_call_failed | internal | null
failure_detailstring | null
started_at *string (date-time)
finished_atstring | null (date-time)

Usage

Today's credits, in one unit, priced by what a call does (requests 1, management 1, actions 2, snapshots 10, uploads 50, stream_minutes 20). The three layers are reported separately because the remedies differ: included_credits is free and resets at UTC midnight, credit_balance is bought and pays for the part of a day above the allowance, and credits.budget is a ceiling that refuses whether or not there is a balance. The standard figures are 6,000 included and 35,000 of ceiling per reachable phone, each plus a flat 2,500.

CampoTipoDescripción
creditsBudgetThe only ceiling. Everything else here explains it.
included_creditsintegerToday's free allowance. Past it a call draws the part of its price above the line from credit_balance, or is refused credits_exhausted.
paid_creditsintegerHow much of today's spend came out of the balance.
credit_balanceintegerPurchased credits in hand. Held by the agency, shared by its keys and connections, never reset.
spent_onobjectWhere today's credits went: one WorkSpend per kind of work, under the names priced above. The credits add up to credits.used.
priceobjectWhat one unit of each kind costs today. Read it, do not hardcode it.
reachable_devicesintegerHow many phones this key reaches: the multiplier on the allowance and on the ceiling, floored at 3. Zero leaves only the flat part. A capacity figure, so it counts a handset no relay reports any more and will read higher than GET /devices lists. That is deliberate: a phone dropping off a rack must not shrink the budget you paid for.
devicesobjectToday's spend per phone, keyed by public_id, which is what shows where a shared pool went.
active_streamsinteger
max_concurrent_streamsinteger
resets_atstring (date-time)

WorkSpend

How much of one kind of work was done, and what it cost. count is units of that kind: calls, taps, screenshots, files, minutes of relay.

CampoTipoDescripción
countinteger
creditsinteger

Group

CampoTipoDescripción
id *string (uuid)
name *string (max length 64)
device_count *integerEvery phone in the group, including ones this key cannot reach. The ids on this list are the ones PUT /devices/{id}/groups accepts, so hiding a group the rest of the rack uses would make that call refuse an id you can see in your dashboard.

SetDeviceGroups

CampoTipoDescripción
group_ids *array (max items 50)The complete set. An empty array files the phone under nothing.
new_group_namestring (max length 64)Create a group and put the phone in it. Reuses an existing group of the same name rather than making a second. An agency may hold at most 50.

Period

The window a report covers, and the clock its days are drawn on.

CampoTipoDescripción
from *string (date)
to *string (date)
days *integer
timezone *stringIANA name, from the agency's own setting. A VA shift running 09:00-18:00 in the Americas straddles UTC midnight, so rolling up by UTC date would split one working day across two rows.

AnalyticsSummary

CampoTipoDescripción
period *Period
active_minutes *integer
users_activeinteger
phones_activeintegerPhones someone touched in the period, counted only over the phones in phones_held so it never reads above it. One the agency has since handed back, or that no relay reports any more, drops out, which is why this can read lower than a day in trend.
phones_heldintegerPhones the agency holds, whether or not anyone touched them. Leaves out a handset no relay reports any more, so it can read lower than the phones_held on the agency object, which counts every phone against phone_limit.
gesturesinteger
viewed_secondsinteger
untrusted_minutesintegerThe part of active_minutes that arrived too late to corroborate. Included in the total, not additional to it.
last_activity_atstring | null (date-time)When a usage report last arrived, across the whole retained window rather than this period. The "is collection working" signal.
trend *arrayOne point per day, oldest first.

MemberUsage

CampoTipoDescripción
member_id *string (uuid)
username *string
rolesarrayRole names. Somebody can hold more than one.
active_minutes *integer
presence_minutesintegerDesk time, with overlapping sittings merged so six phones at once is one minute. What a timesheet is built from.
phones_touchedintegerThe most phones this person used on any one day of the period, not the distinct phones across it. Somebody working five handsets a day all week reads 5, and may well have touched eight different ones. For the distinct set, count the devices array from ?include=devices. That array is limited to the phones this key reaches and this figure is not, so a key scoped to part of the rack sees a shorter array than this number.
sessionsintegerSeparate stretches at a phone, on any phone in the agency. Bounded by the sitting retention rather than the analytics one, so a long enough period can show worked hours with no sittings left behind them. GET /analytics/sessions itemises the same sittings but is limited to the phones this key reaches, so a key scoped to part of the rack returns fewer rows than this counts.
gesturesinteger
viewed_secondsinteger
last_active_atstring | null (date-time)
untrusted_minutesinteger
devicesMemberDeviceUsage[]Only with ?include=devices. Present as [] rather than absent when asked for and there is nothing, so a client can index it either way.

MemberUsageReport

CampoTipoDescripción
period *Period
members *MemberUsage[]

DeviceUsageReport

CampoTipoDescripción
period *Period
devices *DeviceUsage[]
untouched *DeviceUsage[]Reachable phones nobody touched in the period. The line item that costs money.

ActivityReport

CampoTipoDescripción
period *Period
cells *arrayWorked minutes by hour of the agency's local week, cells[day][hour], Monday first. Always 7 rows of 24, so a quiet hour is a zero rather than a gap and you can index it without bounds checks.
peak *integerThe busiest cell, for scaling a colour ramp in one pass.

MemberDeviceUsage

One person's totals on one phone over the period, from the daily rollup. active_minutes are phone-minutes.

CampoTipoDescripción
public_id *string
namestring | null
inventory_idstring | null
active_minutes *integer
gesturesinteger
tapsinteger
swipesinteger
textsinteger
keysintegerIndividual key presses. Typed text is counted, never stored.
viewed_secondsinteger
last_active_atstring | null (date-time)

SessionPage

CampoTipoDescripción
period *Period
sessions *Session[]
next_cursor *string | nullPass back as ?cursor=. Null on the last page.
activity_from *string (date-time)Sittings starting before this report active_minutes and gestures as null: the per-minute grain behind them is kept for 8 days and then dropped a partition at a time.
retention_days *integerHow far back sittings are kept at all. Beyond this the list is empty.

Session

One person, one phone, one stretch.

CampoTipoDescripción
session_id *string (uuid)
member_idstring | null (uuid)Null for a support sitting, along with the username.
usernamestring | null
supportbooleanSomebody from ALI rather than one of your own people. That we were on your phone is yours to know; which of us is not.
device_public_id *string
device_namestring | null
inventory_idstring | null
started_at *string (date-time)
ended_at *string (date-time)
minutes *integerWall clock, rounded to the minute.
active_minutesinteger | nullMinutes inside the sitting that carried a gesture. Can exceed minutes on a short one, because the minute is the grain. Null past activity_from.
gesturesinteger | nullCompleted press-to-release gestures, not the pointer stream they decompose into. Null past activity_from.

DeviceUsage

CampoTipoDescripción
public_id *string
namestring | null
inventory_idstring | null
active_minutes *integer
users_touchedinteger
gesturesinteger
viewed_secondsinteger
last_active_atstring | null (date-time)

Member

CampoTipoDescripción
id *string (uuid)
username *string
email *string (email)
roles *arrayEvery role held, highest standing first. What they may do is the union.
email_verifiedboolean
two_factor_enabledboolean
phone_grantsintegerHow many phones are granted to this person individually.
created_atstring (date-time)

SetMemberRoles

CampoTipoDescripción
role_ids *array (max items 50)The complete set, never a delta. An empty array removes every role, which leaves the account able to do nothing until somebody gives it one.

Role

CampoTipoDescripción
id *string (uuid)
name *string
descriptionstring | null
rank *integerStanding. Higher acts on lower; the three seeded into every agency are 100 (owner), 50 (admin) and 10 (member).
system_keyowner | admin | member | nullSet on the seeded roles, null on anything the agency made itself.
permissions *array
member_countinteger
assignable *booleanWhether this key may hand this role out. A key carries the standing of whoever created it, so this is per-key rather than a property of the role.

Invitation

CampoTipoDescripción
id *string (uuid)
email *string (email)
role *{ id: string (uuid), name: string }
status *sending | sent | opened | clicked | accepted | expired | revoked | failedRead opened with care: an open is an image load, so clients that prefetch report one nobody saw. A clicked or an accepted is a fact.
invited_bystring | null
created_atstring (date-time)
expires_atstring (date-time)
accepted_atstring | null (date-time)

CreateInvitation

CampoTipoDescripción
email *string (max length 254, email)An address that already has an account anywhere is refused: a person belongs to exactly one agency.
role_id *string (uuid)From GET /roles. Only the ones marked assignable will be accepted.

Agency

CampoTipoDescripción
id *string (uuid)
name *string
status *stringactive, not-active, and the other values the billing lifecycle sets.
disabledbooleanAlways false here: a disabled agency's key is refused with agency_disabled before it reaches this endpoint.
timezone *stringIANA name. The day boundary every analytics figure is drawn on.
phone_limitinteger | null
phones_heldintegerPhones assigned to the agency, counted against phone_limit. Includes a handset no relay reports any more, which the analytics summary's phones_held leaves out.
member_countinteger
requires_two_factorbooleanWhether every member must have two-factor confirmed to sign in.
created_atstring (date-time)

Problem

RFC 7807 problem document. Integrate against code, which is stable; detail is prose and may be reworded.

CampoTipoDescripción
typestring
titlestring
status *integer
code *quota_exceeded | max_active_devices_reached | rate_limited | device_offline | kernel_timeout | device_not_found | command_rejected | invalid_action | unauthorized | forbidden | scope_required | payload_too_large | upload_failed | internal | agency_disabled | key_unscoped | invalid_request | idempotency_key_reused | idempotency_in_flight | device_unprovisioned | device_blocked | device_not_provisioned_for_action | not_found | device_in_use | rotation_unavailable | rotation_in_progress | key_orphaned | credits_exhausted
detail *string
request_idstringQuote this when contacting support.

Errores

Todo fallo es un documento de problema RFC 7807. Ramifica según code, nunca según detail, que puede reescribirse.

CódigoEstadoSignificado
quota_exceeded429Daily quota exhausted. The daily ceiling resets at UTC midnight and refuses with or without purchased credits
max_active_devices_reached429Too many devices in use at once. Close a stream before opening another
rate_limited429Too many requests. Back off and honour Retry-After
device_offline503Device unreachable. The phone may not be casting; send a recast action
kernel_timeout504Device did not respond in time. Retry: the phone was reachable but slow
device_not_found404No such device. The id is wrong, or this key cannot reach that phone
command_rejected422The device rejected the command. The phone refused it; detail says what to try next
invalid_action400The action failed validation. detail names the field that failed
unauthorized401Missing or invalid API key. Send Authorization: Bearer ali_live_...
forbidden403Not permitted. The key is valid but not permitted to do this
scope_required403The key lacks a required scope. Add the scope to the key, or use one that has it
payload_too_large413Payload too large. Split the upload or reduce the file
upload_failed502Upload could not be completed. Retry the upload
internal500Something went wrong on our side. Quote the request_id if you contact support
agency_disabled403This account is disabled. The key is valid and works again once the account does
key_unscoped403This key reaches no devices. Its creator may have left; re-scope the key in the dashboard
invalid_request400The request could not be understood. Check the body against the schema above
idempotency_key_reused422This Idempotency-Key was used for a different request
idempotency_in_flight409The first request with this Idempotency-Key is still running
device_unprovisioned409Device is not connected to a box. The phone is not wired up, or the hardware serving it is mid-update
device_blocked403This phone is blocked
device_not_provisioned_for_action409The phone's on-device helper did not answer in time. On open_url it usually sat on the iOS approval prompt for that destination; tell us the phone and the link if it keeps refusing
not_found404Not found. Check the path and the id
device_in_use409Device in use. Somebody is driving this phone from the dashboard, or another key is. Wait, or ask them to hand it over
rotation_unavailable409This device cannot rotate its IP
rotation_in_progress409An IP rotation is already running
key_orphaned403The account this key acts for is gone. The account that created this key is gone from the agency; mint a replacement under one that is still active
credits_exhausted402Out of credits. Buy credits in the dashboard, or wait for the allowance to reset at UTC midnight