Developers

API reference

Drive your phones from your own code, and run the agency around them.

Base URLhttps://beta.aliremote.com/api/v1OpenAPI/api/v1/openapi.jsonFor language models/llms-full.txt
On this page

Authentication

Every request carries an API key. Create one in the dashboard under API access, scoped to one agency and to a set of scopes and phones. The secret is shown once.

Authorization: Bearer ali_live_<key_id>_<secret>

An endpoint answers 403 scope_required if the key lacks the scope, 403 forbidden if its creator could not do it in the dashboard either.

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

Conventions

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, the key stops working and needs replacing: those agency endpoints answer 403 key_orphaned and the device endpoints 403 key_unscoped, even when the key was given a fixed list of phones.
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 phone in the agency plus a flat 2,500, free, and reset at UTC midnight. The allowance is the agency's: every key and MCP connection draws on the same one, so a new or rotated key does not start the day over. 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, for each key, 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 ceiling 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 and what the whole agency has spent against it today, the balance, and what this key 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

Scope: devices.read

List your devices

Details

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.

ParameterInTypeDescription
cursorquerystringThe next_cursor from the previous page.
limitqueryinteger (min 1, max 200, default 50)

Returns DevicePage

StatusMeaning
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}

Scope: devices.read

One device

Details
ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).

Returns Device

StatusMeaning
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}

Scope: devices.write

Rename a device, or set its inventory sticker

Details

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.

ParameterInTypeDescription
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.

Returns Device

StatusMeaning
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"

GET/devices/{id}/values

Scope: devices.read

What a phone keeps

Details

The named values kept with a phone: what automations have recorded about it, such as an account's status, and what has been set for it, such as its handle or link. Each says when it was last set, from where, and by which automation. An automation reads the same values as {{phone.kept.name}}.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
StatusMeaning
200The values, by name.
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/values" -H "Authorization: Bearer $ALI_API_KEY"

PUT/devices/{id}/values/{name}

Scope: devices.write

Keep a value with a phone

Details

Sets one named value, replacing what was there. This is how a phone is given what is its own before an automation runs across a rack: its handle, its link, the caption it should post. The value is text of up to 2000 characters, a number, or true or false, and a phone keeps at most 100. Needs devices.write and phones.edit on the account that created the key.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
name *pathstring (pattern `^[A-Za-z_][A-Za-z0-9_]{0,39}$`)The name it is kept under: letters, digits and underscores, up to 40, not starting with a digit.
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.
StatusMeaning
200Kept. changed is false when it already held that value.
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 PUT "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/values/handle" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value":"lana186935"}'

DELETE/devices/{id}/values/{name}

Scope: devices.write

Remove a value from a phone

Details

Removes one named value. The phone remembers that it was there. Answers 404 when nothing is kept under that name.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
name *pathstring (pattern `^[A-Za-z_][A-Za-z0-9_]{0,39}$`)The name it is kept under: letters, digits and underscores, up to 40, not starting with a digit.
StatusMeaning
200Removed.
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; not_found: Not found
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl -X DELETE "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/values/handle" -H "Authorization: Bearer $ALI_API_KEY"

Seeing the screen

GET/devices/{id}/snapshot

Scope: devices.snapshot

Capture a screenshot

Details

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.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
StatusMeaning
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

Scope: devices.stream

Live screen (WebSocket)

Details

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.

ParameterInTypeDescription
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.
StatusMeaning
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

Scope: devices.control

Rotate the device's public IP

Details

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.

ParameterInTypeDescription
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.

Returns IpRotation

StatusMeaning
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

Scope: devices.read

The device's IP rotations

Details

The rotations run on this device while it belonged to your agency, newest first.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
StatusMeaning
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

Scope: devices.control

Send an action to a device

Details

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. A phone also carries out one thing at a time. An action that reaches it while its queue is already full, such as a gesture arriving while one is being drawn and another is already waiting behind it, is answered 504 kernel_timeout with a Retry-After header, and nothing was sent to the phone. Wait that many seconds and send it again. The phone is not offline, so do not recast it. 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 a finger placed, pressed, rested, walked and released at the end point. swipe, drag and swipe_direction walk the flick the dashboard's arrows send, eight steps and about a third of a second in all. scroll and hold_drag walk sixteen slower steps, a little over half a second, because they are read by distance and a list flung at the flick's pace keeps rolling after the finger lifts. swipe and drag take the finger from (x1,y1) to (x2,y2) and are drawn the same way. hold_drag walks that same line after holding the press still for hold_ms (1200 by default, 100 to 5000), which is what the phone waits for before it lifts a home screen icon to rearrange it or picks up an item to drop somewhere else; anything that moves as soon as it is touched, such as a slider or an app switcher card, drags with a plain swipe, which is about a second quicker. 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 number of 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. hold_drag takes no precise and is never varied, because its press has to land on what it lifts. 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 or query 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.

ParameterInTypeDescription
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.

Returns ActionResult

Each action is one object, chosen by type. * marks a required field.

TypeFields
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
hold_dragx1* integer (min 0, max 10000), y1* integer (min 0, max 10000), x2* integer (min 0, max 10000), y2* integer (min 0, max 10000), hold_ms integer (min 100, max 5000, default 1200)
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)
app_launchurl* 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
StatusMeaning
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

Scope: devices.read

See who is driving a device

Details

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.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
StatusMeaning
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

Scope: devices.control

Declare that an automation is driving a device

Details

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. Opening a device in the dashboard never takes it, so a person can watch your script run live without it ever answering device_in_use. While a session is declared, touching the screen does not take it either: a person has to press Take control. Without one, the phone is free once the lease from your last call lapses, and touching it then takes it. 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.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).

Takes ControlSessionRequest

StatusMeaning
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

Scope: devices.control

End an automation's session

Details

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

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
StatusMeaning
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

Scope: devices.control

Take a device somebody else is driving

Details

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. Watching a phone in the dashboard does not hold it, so a phone is busy here because somebody has touched it within the last few minutes. 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.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).

Takes ControlTakeoverRequest

StatusMeaning
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"

Media vault

GET/devices/{id}/media

Scope: media.read

What the vault sent to a device

Details

What the vault sent to this phone, newest first, each with its delivery. This is our record of what we put there, not a read of the phone's camera roll: photos taken or saved on the phone any other way do not appear. When the record and the phone may disagree, POST /media/reconcile re-checks it.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
cursorquerystringThe next_cursor from the previous page.
limitqueryinteger (min 1, max 60, default 24)

Returns DeviceMediaPage

StatusMeaning
200A page of deliveries.
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/media" -H "Authorization: Bearer $ALI_API_KEY"

POST/devices/{id}/media

Scope: media.write

Start sending a file to a device

Details

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_bytes → 201 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}/complete → 202. 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. This is the quick route for one file to one phone. The file also lands in the media vault, where /media can send it on to more phones, organise it, or take it back off: see GET /media.

ParameterInTypeDescription
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.

Takes UploadRequest · Returns UploadTicket

StatusMeaning
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

Scope: media.write

Finish an upload and queue the delivery

Details

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.

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

Returns UploadAccepted

StatusMeaning
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}

Scope: media.write

Take a file back off a device

Details

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 file still waiting to be sent never reached the phone, so it is called off at once and answers 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 being sent to the phone answers command_rejected: wait for it to land 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.

ParameterInTypeDescription
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.
StatusMeaning
202Removal queued. Poll GET on this same path until removed. A file that had not been sent yet answers removed straight away.
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}

Scope: devices.read

Whether an upload landed

Details
ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
asset_id *pathstring

Returns Delivery

StatusMeaning
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"

GET/media

Scope: media.read

Browse the media vault

Details

The media vault is the agency's shared library of photos and videos: the same one the dashboard shows under Media vault. Everything a person can do there, a key can do here. Three scopes, so a key only holds what it needs. - media.read browses: files, folders, tags, and what went to which phone. - media.write moves files: uploads into the vault, sends them to phones, and takes them back off. - media.manage organises: folders, tags, posted, and deleting from the vault. The life of a file. Put it in the vault (POST /media then PUT then /complete, or POST /media/import from a link), send it to phones (POST /media/push), follow the deliveries (GET /media/{asset_id}, or the media.* events on /events), and take it back off when you are done (POST /media/remove). Files expire. A file is kept for 7 days after it is uploaded, and expires_at says when. A vault holds at most 1000 live files at a time; usage on GET /media says how full it is, and an upload into a full vault answers 409 vault_full. Links are short-lived. urls are signed and stop working after 60 minutes (urls_expire_at). Read the file again for fresh ones rather than storing them. Deleting from the vault does not touch the phones. A deleted file can no longer be taken off a phone through ALI, so remove it from the phones first if it should go from both.

ParameterInTypeDescription
cursorquerystringThe next_cursor from the previous page.
limitqueryinteger (min 1, max 60, default 24)
sortquery`created` | `taken`created is upload order; taken is when it was shot, where known. Newest first either way.
folder_idquerystringOnly files in this folder. none means files in no folder.
include_subfoldersqueryboolean (default false)
tag_idquerystring (uuid)Repeat to require several: a file must carry every one.
postedqueryboolean
kindquery`image` | `video`
searchquerystring (max length 100)Only files whose name contains this, ignoring case.

Returns MediaPage

StatusMeaning
200A page of ready files.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/media?folder_id=FOLDER_ID&posted=false&limit=24" -H "Authorization: Bearer $ALI_API_KEY"

POST/media

Scope: media.write

Start an upload into the vault

Details

Uploading into the vault is three steps, and the bytes never pass through the API: 1. POST /media with the file's filename, mime and size_bytes → 201 with an upload_url. 2. PUT the raw bytes to upload_url before upload_expires_at, with the Content-Type given in upload_headers. 3. POST /media/{asset_id}/complete → 200 with the stored file. Storage is checked for what actually arrived, size and type against what was declared, and the file becomes ready. Nothing is sent to a phone: that is POST /media/push. To upload for one phone in one go, POST /devices/{id}/media still does both. At most 100 MB per file. Costs one uploads (50 credits).

ParameterInTypeDescription
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.

Takes VaultUploadRequest · Returns VaultUploadTicket

StatusMeaning
201A slot is reserved for 15 minutes.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409vault_full: The media vault is full
413payload_too_large: Payload too large
429quota_exceeded: Daily quota exhausted; rate_limited: Too many requests
# 1. describe the file; a presigned URL comes back
curl -X POST "https://beta.aliremote.com/api/v1/media" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"look-3.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 @look-3.jpg

# 3. confirm; the file is now ready to push
curl -X POST "https://beta.aliremote.com/api/v1/media/$ASSET_ID/complete" -H "Authorization: Bearer $ALI_API_KEY"

PATCH/media

Scope: media.manage

Organise several files

Details

Change several files at once: move them into a folder (null for none), mark them posted or not, add tags and take tags away. Give at least one change. Files that are not in the vault are returned under not_found rather than failing the rest. A file may carry at most 12 tags.

ParameterInTypeDescription
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.

Takes MediaBatchEdit · Returns MediaBatchResult

StatusMeaning
200What changed.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409conflict: This clashes with something that already exists
429rate_limited: Too many requests
curl -X PATCH "https://beta.aliremote.com/api/v1/media" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_ids":["ASSET_ID"],"folder_id":"FOLDER_ID","posted":true}'

POST/media/import

Scope: media.write

Import a file from a link

Details

Fetch a photo or video from a link straight into the vault, in one call. Nothing for you to upload: we download it, check it like any upload, and store it. The link must answer with the file itself, over http or https on the standard ports, within 60 seconds and 100 MB. A web page around the file is refused, and so is anything that is not a JPEG, PNG, WebP, GIF, AVIF, HEIC, MP4, MOV or WebM, whatever its name says: the type is read from the bytes. Links to private or internal addresses are refused. A link we will not fetch answers 400 invalid_request; one that fails at the other end (not found, too slow) answers 502 upload_failed, and too large a file 413 payload_too_large. The stored name takes the extension of the file's real type. Answers 201 with the file, ready to push. Costs one uploads (50 credits), because the transfer is ours.

ParameterInTypeDescription
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.

Takes MediaImportRequest · Returns MediaAsset

StatusMeaning
201Stored and ready.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409vault_full: The media vault is full
413payload_too_large: Payload too large
429quota_exceeded: Daily quota exhausted; rate_limited: Too many requests
502upload_failed: Upload could not be completed
curl -X POST "https://beta.aliremote.com/api/v1/media/import" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.example.com/look-3.jpg","tag_ids":["TAG_ID"]}'

POST/media/push

Scope: media.write

Send vault files to phones

Details

Send up to 100 vault files to up to 200 phones in one call. Every phone receives every file, into its camera roll, inside album if you name one. Answers 202 once the deliveries are queued. Getting a file onto a physical handset takes minutes, so follow it with GET /media/{asset_id}, which lists every delivery of that file, or listen for media.delivered and media.failed on /events. Partial success is normal. queued lists the phones the files are on their way to, and skipped every phone that was not, each with a stable reason: | reason | means | |---|---| | device_not_found | no such phone, or this key cannot reach it | | device_blocked | the phone is blocked | | device_suspended | the phone is suspended for billing | | device_in_use | somebody is using the phone; it is skipped rather than interrupted | | rate_limited | too many files sent to this phone just now | skipped_assets does the same for files: not_found, not_ready (the upload never completed) or expired. Only when nothing at all could be queued does the call fail, with the problem that stopped it. Taking the phone for a moment. Like the one-phone upload, a push holds each phone for this key for about thirty seconds, which is what lets it skip a phone somebody is driving instead of landing a file under their finger. Cost. One uploads (50 credits) per phone queued. When today's credits cannot cover every phone, nothing is queued and the call answers 402 credits_exhausted or 429 quota_exceeded. Send an Idempotency-Key: a retry then replays the first answer instead of sending twice. One-time consent. The first time a phone receives anything, iOS may ask whoever holds it to allow it. Until somebody does, deliveries to that phone fail with error_code saying so.

ParameterInTypeDescription
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.

Takes MediaPushRequest · Returns MediaPushResult

StatusMeaning
202Queued, with what was skipped and why.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
402credits_exhausted: Out of credits
403device_blocked: This phone is blocked; key_orphaned: The account this key acts for is gone; key_unscoped: This key reaches no devices; scope_required: The key lacks a required scope
404not_found: Not found; device_not_found: No such device
409device_in_use: Device in use
422command_rejected: The device rejected the command
429quota_exceeded: Daily quota exhausted; rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/media/push" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_ids":["ASSET_ID"],"devices":["ph_0b491ebdeb254769ab26","1277"],"album":"Drafts"}'

POST/media/remove

Scope: media.write

Take vault files back off phones

Details

Take vault files back off phones. A file recorded as delivered to a phone is removed from it, and one still queued for it is called off before it is sent; the files stay in the vault. Answers 202 with the phones a removal is on its way to. A removal is confirmed by reading the phone back afterwards, never by trusting the delete itself, so a delivery reads removed only once the file is really gone. One that could not be confirmed goes back to delivered with an error saying why, and media.remove_failed fires on /events. Costs one uploads (50 credits) per phone a removal is queued on. As with a push, when today's credits cannot cover every phone, nothing is queued. skipped reasons are those of a push except device_in_use, plus nothing_delivered (none of those files are on that phone), push_in_flight (some of those files are being sent to that phone right now; the ones still waiting are called off, and nothing else is removed there until these land, so ask again then) and removal_in_progress (a removal of those files is already under way there). One-time consent. The first removal on a phone can wait on an iOS prompt that somebody must accept on the handset once. Until then the file stays put, and the delivery says so with error_code: approval_needed_on_phone rather than reporting a clean removal.

ParameterInTypeDescription
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.

Takes MediaDevicesRequest · Returns MediaRemoveResult

StatusMeaning
202Removal queued.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
402credits_exhausted: Out of credits
403device_blocked: This phone is blocked; key_orphaned: The account this key acts for is gone; key_unscoped: This key reaches no devices; scope_required: The key lacks a required scope
404not_found: Not found; device_not_found: No such device
422command_rejected: The device rejected the command
429quota_exceeded: Daily quota exhausted; rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/media/remove" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_ids":["ASSET_ID"],"devices":["ph_0b491ebdeb254769ab26"]}'

POST/media/reconcile

Scope: media.write

Re-check what is on phones

Details

Re-read phones and correct the record of which vault files are on them. A delivery can end failed for a file that landed after all, for instance when the phone answered too slowly. This checks each phone for the files we have on record for it, marks what is there as delivered and what is gone as removed. It runs in the background: read the outcome from GET /devices/{id}/media or GET /media/{asset_id} a minute later. Costs one uploads (50 credits) per phone checked, since it reads the handset. As with a push, when today's credits cannot cover every phone, nothing is queued. skipped reasons are those of a push except device_in_use, plus nothing_to_check for a phone nothing from the vault was ever sent to.

ParameterInTypeDescription
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.

Takes MediaReconcileRequest · Returns MediaReconcileResult

StatusMeaning
202The check is running.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
402credits_exhausted: Out of credits
403device_blocked: This phone is blocked; key_orphaned: The account this key acts for is gone; key_unscoped: This key reaches no devices; scope_required: The key lacks a required scope
404not_found: Not found; device_not_found: No such device
422command_rejected: The device rejected the command
429quota_exceeded: Daily quota exhausted; rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/media/reconcile" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"devices":["ph_0b491ebdeb254769ab26"]}'

POST/media/delete

Scope: media.manage

Delete several files from the vault

Details

Delete up to 100 files from the vault for good. The phones are not touched: take the files off them first with POST /media/remove if they should go from there too, because a deleted file can no longer be removed through ALI.

ParameterInTypeDescription
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.

Takes MediaAssetIds · Returns MediaDeleteResult

StatusMeaning
200What was deleted.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/media/delete" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_ids":["ASSET_ID"]}'

GET/media/{asset_id}

Scope: media.read

One file, and where it went

Details

The file, fresh signed links, and every delivery of it to a phone this key reaches: the one call that follows a push across all its phones. Answers for a file still awaiting_upload too.

ParameterInTypeDescription
asset_id *pathstring (uuid)The file's id, from GET /media or the upload that created it.

Returns MediaDetail

StatusMeaning
200The file.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/media/ASSET_ID" -H "Authorization: Bearer $ALI_API_KEY"

PATCH/media/{asset_id}

Scope: media.manage

Organise one file

Details

Change several files at once: move them into a folder (null for none), mark them posted or not, add tags and take tags away. Give at least one change. Files that are not in the vault are returned under not_found rather than failing the rest. A file may carry at most 12 tags.

ParameterInTypeDescription
asset_id *pathstring (uuid)The file's id, from GET /media or the upload that created it.
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.

Takes MediaEdit · Returns MediaAsset

StatusMeaning
200The file as it is now.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409conflict: This clashes with something that already exists
429rate_limited: Too many requests
curl -X PATCH "https://beta.aliremote.com/api/v1/media/ASSET_ID" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"add_tag_ids":["TAG_ID"],"posted":true}'

DELETE/media/{asset_id}

Scope: media.manage

Delete one file from the vault

Details

Deletes the file for good. The phones it was sent to are not touched; remove it from them first with POST /media/remove if it should go from there too.

ParameterInTypeDescription
asset_id *pathstring (uuid)The file's id, from GET /media or the upload that created it.
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.

Returns MediaDeleteResult

StatusMeaning
200Deleted.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
curl -X DELETE "https://beta.aliremote.com/api/v1/media/ASSET_ID" -H "Authorization: Bearer $ALI_API_KEY"

POST/media/{asset_id}/complete

Scope: media.write

Finish an upload into the vault

Details

Call after PUTting the bytes to upload_url. Storage is checked for what actually arrived, size and type against what was declared, and the file becomes ready. A file that does not match is refused with upload_failed and discarded, so start again with the real details. Calling it again on a ready file just returns it.

ParameterInTypeDescription
asset_id *pathstring (uuid)The file's id, from GET /media or the upload that created it.
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.

Returns MediaAsset

StatusMeaning
200Stored and ready.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
502upload_failed: Upload could not be completed
curl -X POST "https://beta.aliremote.com/api/v1/media/ASSET_ID/complete" -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" -d '{}'

GET/media/folders

Scope: media.read

List vault folders

Details

Every folder, flat, with parent_id so you can rebuild the tree. unfiled counts files in no folder.

Returns MediaFolderList

StatusMeaning
200The folders.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/media/folders" -H "Authorization: Bearer $ALI_API_KEY"

POST/media/folders

Scope: media.manage

Create a vault folder

Details

At the top, or inside parent_id. Folders nest 4 deep, a vault holds at most 200, and names are unique among siblings: a clash answers 409 conflict.

ParameterInTypeDescription
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.

Takes MediaFolderRequest · Returns MediaFolder

StatusMeaning
201Created.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409conflict: This clashes with something that already exists
429rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/media/folders" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"October drops","parent_id":null}'

PATCH/media/folders/{folder_id}

Scope: media.manage

Rename or move a vault folder

Details

name, parent_id, or both. parent_id: null moves it to the top. A folder cannot move inside itself.

ParameterInTypeDescription
folder_id *pathstring (uuid)The folder's id, from GET /media/folders.
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.

Takes MediaFolderRequest · Returns MediaFolder

StatusMeaning
200The folder as it is now.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409conflict: This clashes with something that already exists
429rate_limited: Too many requests
curl -X PATCH "https://beta.aliremote.com/api/v1/media/folders/FOLDER_ID" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"October drops","parent_id":null}'

DELETE/media/folders/{folder_id}

Scope: media.manage

Delete a vault folder

Details

Deletes the folder and every folder beneath it. The files inside are kept and become unfiled.

ParameterInTypeDescription
folder_id *pathstring (uuid)The folder's id, from GET /media/folders.
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.
StatusMeaning
200Deleted.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
curl -X DELETE "https://beta.aliremote.com/api/v1/media/folders/FOLDER_ID" -H "Authorization: Bearer $ALI_API_KEY"

GET/media/tags

Scope: media.read

List vault tags

Details

Returns MediaTagList

StatusMeaning
200The tags.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
429rate_limited: Too many requests
curl "https://beta.aliremote.com/api/v1/media/tags" -H "Authorization: Bearer $ALI_API_KEY"

POST/media/tags

Scope: media.manage

Create a vault tag

Details

Names are unique in the vault, which holds at most 100 tags. Attach one with PATCH /media.

ParameterInTypeDescription
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.

Takes MediaTagRequest · Returns MediaTag

StatusMeaning
201Created.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
409conflict: This clashes with something that already exists
429rate_limited: Too many requests
curl -X POST "https://beta.aliremote.com/api/v1/media/tags" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"approved"}'

PATCH/media/tags/{tag_id}

Scope: media.manage

Rename a vault tag

Details
ParameterInTypeDescription
tag_id *pathstring (uuid)The tag's id, from GET /media/tags.
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.

Takes MediaTagRequest · Returns MediaTag

StatusMeaning
200The tag as it is now.
400invalid_request: The request could not be understood
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
409conflict: This clashes with something that already exists
429rate_limited: Too many requests
curl -X PATCH "https://beta.aliremote.com/api/v1/media/tags/TAG_ID" \
  -H "Authorization: Bearer $ALI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"approved"}'

DELETE/media/tags/{tag_id}

Scope: media.manage

Delete a vault tag

Details

The tag comes off every file that carried it. The files are kept.

ParameterInTypeDescription
tag_id *pathstring (uuid)The tag's id, from GET /media/tags.
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.
StatusMeaning
200Deleted.
401unauthorized: Missing or invalid API key
403key_orphaned: The account this key acts for is gone; scope_required: The key lacks a required scope
404not_found: Not found
429rate_limited: Too many requests
curl -X DELETE "https://beta.aliremote.com/api/v1/media/tags/TAG_ID" -H "Authorization: Bearer $ALI_API_KEY"

Automations

GET/automations

Scope: automations.read

List automations

Details

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. A key limited to particular phones keeps its runs, and the runs of the triggers it adds, on those phones. Revoking a key, or letting it expire, stops those runs and triggers; rotating it hands them to the new key. 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.

StatusMeaning
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

Scope: automations.write

Create an automation

Details

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.

ParameterInTypeDescription
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.
StatusMeaning
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}

Scope: automations.read

Get one automation

Details

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.

ParameterInTypeDescription
id *pathstring (uuid)The automation's id, from GET /automations.
StatusMeaning
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}

Scope: automations.write

Rename, switch on, or save a new draft

Details

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.

ParameterInTypeDescription
id *pathstring (uuid)The automation's id, from GET /automations.
StatusMeaning
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}

Scope: automations.write

Delete an automation

Details

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

ParameterInTypeDescription
id *pathstring (uuid)The automation's id, from GET /automations.
StatusMeaning
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

Scope: automations.write

Publish the current draft

Details

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.

ParameterInTypeDescription
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.
StatusMeaning
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

Scope: automations.read

List this automation's runs

Details

Newest first, with how each one ended.

ParameterInTypeDescription
id *pathstring (uuid)The automation's id, from GET /automations.
StatusMeaning
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

Scope: automations.write

Start a run now

Details

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. A key limited to particular phones keeps its runs on those phones, even where that person reaches more. An automation that declares inputs takes a value for each under inputs, by name. One with a default may be left out. A run that is missing a value it needs, is given one of the wrong type, or is given a name the automation does not declare is refused with every problem at once, before it touches a phone.

ParameterInTypeDescription
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.
StatusMeaning
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

Scope: automations.write

Add a trigger

Details

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. The runs a trigger starts stay on the phones of the key that added it. Steps limited to ALI staff fail in those runs when the key is limited to particular phones.

ParameterInTypeDescription
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.
StatusMeaning
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}

Scope: automations.write

Remove a trigger

Details

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

ParameterInTypeDescription
id *pathstring (uuid)The automation's id, from GET /automations.
trigger_id *pathstring (uuid)The trigger's id, from GET /automations/{id}.
StatusMeaning
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

Scope: automations.read

List the steps a flow can be built from

Details

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.

StatusMeaning
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

Scope: automations.read

List recent runs

Details

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

ParameterInTypeDescription
automation_idquerystring (uuid)Only this automation's runs.
limitqueryinteger (min 1, max 200)Defaults to 50.
StatusMeaning
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}

Scope: automations.read

Get one run, step by step

Details

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. output is what the run handed back through its return step, or null when it has none or has not finished. calls lists the runs this one started with a run_automation step, each with its own run_id to read the same way, and parent names the run that started this one, if any did. phones is set once the run loops over phones: total is how many it set out to work through, finished how many got to the end of the loop, and failed names each phone that did not, with the reason (up to 500). A step that fails on one phone moves on to the next unless the loop is set to stop the whole run, and a run counts as failed only when none of its phones finished.

ParameterInTypeDescription
id *pathstring (uuid)The run's id.
StatusMeaning
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"

GET/automation-results/datasets

Scope: automations.read

List the kinds of result recorded

Details

The names automations have recorded results under, such as account_status, with how many rows each holds, how many different things they are about, and when the last was recorded. Limited to the phones this key reaches: the counts include rows from those phones and rows about no phone.

StatusMeaning
200The datasets.
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-results/datasets" -H "Authorization: Bearer $ALI_API_KEY"

GET/automation-results

Scope: automations.read

Read recorded results

Details

The rows automations have recorded with a record step: structured results such as each account's status or each reel's numbers. Each row carries its values under data, which keeps whatever shape the flow gave it, the phone it came from, an optional subject when the row is about something other than the phone, and changed, which is false when it matches the row before about the same thing. latest=true gives only the newest row about each phone or subject, which is the current picture across the rack. changed=true keeps only rows that differ from the one before. Page backwards with before set to next_before. format=csv answers the same rows as a file, one column per value, nested values named by their path. Limited to the phones this key reaches: rows from other phones are left out, and rows about no phone are always included.

ParameterInTypeDescription
datasetquerystring
devicequerystringA phone's public_id.
subjectquerystring
latestqueryboolean
changedqueryboolean
sincequerystring (date-time)
beforequeryinteger
limitqueryinteger (min 1, max 5000)
formatquery`json` | `csv`
StatusMeaning
200The rows, newest first.
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
429rate_limited: Too many requests; quota_exceeded: Daily quota exhausted
curl "https://beta.aliremote.com/api/v1/automation-results" -H "Authorization: Bearer $ALI_API_KEY"

GET/automation-catalogue

Scope: automations.read

Browse the catalogue

Details

The ready-made automations ALI publishes for every agency. Each entry says what it does, and needs says what it asks for: the inputs a run is given, the values each phone must kept for it such as its handle, the links it opens (each needs allowing once on a phone), the screens it was measured on, and whether it runs on_phone. installed is set when this agency has already added it.

StatusMeaning
200The catalogue.
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-catalogue" -H "Authorization: Bearer $ALI_API_KEY"

POST/automation-catalogue/{id}/install

Scope: automations.write

Add a catalogue automation to your agency

Details

Adds the entry as an automation of your own, to run, schedule and use from your flows through the same endpoints as any other. Its steps stay ALI's and cannot be changed. By default it follows new releases, so a fix made once reaches you; send follows: false to keep the version it was added at. Adding one you already have answers the same automation.

ParameterInTypeDescription
id *pathstring (uuid)
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.
StatusMeaning
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/automation-catalogue/ph_0b491ebdeb254769ab26/install" -H "Authorization: Bearer $ALI_API_KEY"

GET/automation-batches/{id}

Scope: automations.read

Get a batch of runs

Details

How an automation started on several phones is going: counts by status, finished once every run has ended, and for each phone its run_id, status, error and output.

ParameterInTypeDescription
id *pathstring (uuid)
StatusMeaning
200The batch and its 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/automation-batches/ph_0b491ebdeb254769ab26" -H "Authorization: Bearer $ALI_API_KEY"

GET/devices/{id}/automation-runs

Scope: automations.read

Runs started on a phone

Details

The most recent runs started on this phone, newest first, each with how it ended and what it handed back. Runs that chose their own phones in a loop are not listed here.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).
StatusMeaning
200Recent runs on this phone.
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
curl "https://beta.aliremote.com/api/v1/devices/ph_0b491ebdeb254769ab26/automation-runs" -H "Authorization: Bearer $ALI_API_KEY"

POST/automation-runs/{id}/cancel

Scope: automations.write

Stop a run

Details

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. A key limited to particular phones stops the runs it started and the runs on its phones, and is refused any other with forbidden.

ParameterInTypeDescription
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.
StatusMeaning
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

Scope: devices.read

List your groups

Details

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.

StatusMeaning
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

Scope: groups.write

Set the groups a device is in

Details

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.

ParameterInTypeDescription
id *pathstringThe device's public_id, or its sticker (inventory_id).

Takes SetDeviceGroups

StatusMeaning
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

Scope: analytics.read

Agency totals and the daily trend

Details

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.

ParameterInTypeDescription
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.

Returns AnalyticsSummary

StatusMeaning
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

Scope: analytics.read

What each person did

Details

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.

ParameterInTypeDescription
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.

Returns MemberUsageReport

StatusMeaning
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

Scope: analytics.read

What each phone carried, and which carried nothing

Details

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. A phone no relay reports any more is left out, hours and all, the same way phones_held on GET /analytics/summary leaves it out. It comes back once a relay reports it again. GET /analytics/members?include=devices still counts the time someone spent on it. 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.

ParameterInTypeDescription
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.

Returns DeviceUsageReport

StatusMeaning
200One row per reachable phone a relay still reports.
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

Scope: analytics.read

Who was on which phone, and when

Details

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.

ParameterInTypeDescription
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.

Returns SessionPage

StatusMeaning
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

Scope: analytics.read

Worked minutes by hour of the week

Details

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.

ParameterInTypeDescription
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.

Returns ActivityReport

StatusMeaning
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

Scope: members.read

List the people in your agency

Details

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.

StatusMeaning
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}

Scope: members.read

One member

Details
ParameterInTypeDescription
id *pathstring (uuid)The member's id, from GET /members.

Returns Member

StatusMeaning
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}

Scope: members.write

Set which roles a member holds

Details

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, and holding no phone they cannot reach themselves. 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.

ParameterInTypeDescription
id *pathstring (uuid)The member's id, from GET /members.

Takes SetMemberRoles · Returns Member

StatusMeaning
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

Scope: members.read

List the roles in your agency

Details

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.

StatusMeaning
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

Scope: members.read

List invitations

Details

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.

StatusMeaning
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

Scope: members.write

Invite somebody into your agency

Details

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. Mail is capped. Every invitation and every resend sends a mail, so an address gets at most one a minute and five an hour from your agency, and the agency twenty an hour across everybody it invites. Like the other limits these are token buckets, so the five and the twenty refill over the hour. A call past them is refused 429 rate_limited with Retry-After and changes nothing. 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.

ParameterInTypeDescription
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.

Takes CreateInvitation · Returns Invitation

StatusMeaning
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}

Scope: members.write

Withdraw an invitation

Details

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.

ParameterInTypeDescription
id *pathstring (uuid)The invitation's id, from GET /invitations.
StatusMeaning
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

Scope: members.write

Send the invitation again

Details

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. It counts toward the same mail caps as the invitation: one a minute and five an hour per address, twenty an hour per agency. A refused resend leaves the previous link working.

ParameterInTypeDescription
id *pathstring (uuid)The invitation's id, from GET /invitations.

Returns Invitation

StatusMeaning
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

Scope: agency.read

The agency this key belongs to

Details

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.

Returns Agency

StatusMeaning
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

Scope: devices.read

Change feed (WebSocket)

Details

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, media.removed, media.remove_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":"media.failed","device":"ph_…","asset_id":"…","error":"…","error_code":"…","at":"…"}
→ {"type":"media.removed","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.

ParameterInTypeDescription
tokenquerystringAs above; prefer the Authorization header.
StatusMeaning
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

Scope: devices.read

Today's credits, allowance and balance

Details

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.

Returns Usage

StatusMeaning
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"

Schemas

The objects these endpoints return and accept. A field marked * is always present.

MediaAsset

A photo or video in the media vault.

FieldTypeDescription
id *string (uuid)
filename *string
mime *string
kind *image | video
size_bytes *integer
status *awaiting_upload | ready | failedawaiting_upload until /complete has checked the bytes; ready from then on; failed if what arrived did not match what was declared.
folder_idstring | null (uuid)
tags *array
posted *booleanMarked as posted by somebody on the team, so the rest know it has gone out.
posted_atstring | null (date-time)
widthinteger | null
heightinteger | null
duration_msinteger | nullVideos only.
taken_atstring | null (date-time)When the photo or video was shot, where the file says.
created_atstring (date-time)
expires_atstring (date-time)When the vault lets it go, 7 days after upload.
camera{ make: string | null, model: string | null, lens: string | null } | null
location{ latitude: number, longitude: number } | nullWhere it was shot, when the file carries it.
urlsobject | nullSigned links, valid for 60 minutes. null until the file is ready.
urls_expire_atstring | null (date-time)

MediaDelivery

One file on its way to, on, or taken off one phone.

FieldTypeDescription
device *stringThe phone's public_id.
asset_id *string (uuid)
status *queued | sending | delivered | failed | removing | removeddelivered with an error means a removal was tried and could not be confirmed, so the file is taken to still be there.
albumstring | null
errorstring | null
error_codestring | nullA stable code beside error, to branch on.
delivered_atstring | null (date-time)
updated_at *string (date-time)

MediaDetail

A file, with every phone this key reaches that it was sent to.

FieldTypeDescription
id *string (uuid)
filename *string
mime *string
kind *image | video
size_bytes *integer
status *awaiting_upload | ready | failedawaiting_upload until /complete has checked the bytes; ready from then on; failed if what arrived did not match what was declared.
folder_idstring | null (uuid)
tags *array
posted *booleanMarked as posted by somebody on the team, so the rest know it has gone out.
posted_atstring | null (date-time)
widthinteger | null
heightinteger | null
duration_msinteger | nullVideos only.
taken_atstring | null (date-time)When the photo or video was shot, where the file says.
created_atstring (date-time)
expires_atstring (date-time)When the vault lets it go, 7 days after upload.
camera{ make: string | null, model: string | null, lens: string | null } | null
location{ latitude: number, longitude: number } | nullWhere it was shot, when the file carries it.
urlsobject | nullSigned links, valid for 60 minutes. null until the file is ready.
urls_expire_atstring | null (date-time)
deliveries *MediaDelivery[]One entry per phone the file was sent to.

MediaPage

FieldTypeDescription
items *MediaAsset[]
next_cursor *string | null
usage *{ active: integer, limit: integer }Live files in the vault against its ceiling.

DeviceMediaPage

FieldTypeDescription
device *string
items *array
next_cursor *string | null

VaultUploadRequest

FieldTypeDescription
filename *string (max length 200)No path separators.
mime *stringe.g. image/jpeg or video/mp4.
size_bytes *integer (max 104857600)
folder_idstring | null (uuid)
tag_idsarray (max items 12)

VaultUploadTicket

FieldTypeDescription
asset_id *string (uuid)
upload_url *stringPresigned PUT for the raw bytes.
upload_expires_at *string (date-time)
upload_headers *{ Content-Type: string }Send these on the PUT. Storage keeps the Content-Type it is given.
status *awaiting_upload

MediaImportRequest

FieldTypeDescription
url *string (max length 2048)A public link to the file itself.
filenamestring (max length 200)Defaults to the name in the link. The extension follows the file's real type.
folder_idstring | null (uuid)
tag_idsarray (max items 12)

MediaEdit

At least one change.

FieldTypeDescription
folder_idstring | null (uuid)Move into this folder. null takes it out of any folder.
postedboolean
add_tag_idsarray (max items 12)
remove_tag_idsarray (max items 12)

MediaBatchEdit

The files to change, and at least one change.

FieldTypeDescription
asset_ids *array (min items 1, max items 100)
folder_idstring | null (uuid)Move into this folder. null takes them out of any folder.
postedboolean
add_tag_idsarray (max items 12)
remove_tag_idsarray (max items 12)

MediaBatchResult

FieldTypeDescription
updatedarray
not_foundarray

MediaDeleteResult

FieldTypeDescription
deletedarray
not_foundarray

MediaPushRequest

FieldTypeDescription
asset_ids *array (min items 1, max items 100)
devices *array (min items 1, max items 200)Each a public_id or an inventory sticker.
albumstring (max length 64)Put the files in this album on each phone, creating it if needed.

SkippedDevice

FieldTypeDescription
device *stringAs you sent it, or its public_id.
reason *device_not_found | device_blocked | device_suspended | device_in_use | rate_limited | nothing_delivered | push_in_flight | removal_in_progress | nothing_to_check
detailstring

MediaPushResult

FieldTypeDescription
status *queued
asset_ids *arrayThe files that are being sent.
queued *arraypublic_id of every phone the files are on their way to.
skipped *SkippedDevice[]
skipped_assets *array
deliveries_queued *integerFiles times phones.

MediaDevicesRequest

FieldTypeDescription
asset_ids *array (min items 1, max items 100)
devices *array (min items 1, max items 200)

MediaRemoveResult

FieldTypeDescription
statusremoving
removingarray
skippedSkippedDevice[]

MediaReconcileResult

FieldTypeDescription
statuschecking
checkingarray
skippedSkippedDevice[]

MediaAssetIds

FieldTypeDescription
asset_ids *array (min items 1, max items 100)

MediaReconcileRequest

FieldTypeDescription
devices *array (min items 1, max items 200)Each a public_id or an inventory sticker.

MediaFolderRequest

Creating needs name. Updating takes name, parent_id, or both.

FieldTypeDescription
namestring (min length 1, max length 64)
parent_idstring | null (uuid)The folder to sit inside. null is the top.

MediaTagRequest

FieldTypeDescription
name *string (min length 1, max length 32)

MediaFolderList

FieldTypeDescription
foldersMediaFolder[]
unfiledintegerFiles in no folder.

MediaTagList

FieldTypeDescription
tagsMediaTag[]

MediaFolder

FieldTypeDescription
idstring (uuid)
parent_idstring | null (uuid)
namestring
depthinteger1 at the top.
file_countintegerFiles directly in this folder.
file_count_with_subfoldersinteger
subfolder_countintegerFolders directly beneath this one.

MediaTag

FieldTypeDescription
idstring (uuid)
namestring
file_countinteger

Device

FieldTypeDescription
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

FieldTypeDescription
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.

FieldTypeDescription
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.

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
asset_id *string
status *queued

Delivery

FieldTypeDescription
asset_id *string
filenamestring
status *awaiting_upload | stored | queued | sending | delivered | failed | removing | removedawaiting_upload and stored describe the upload before /complete has queued it; the rest come from the delivery queue. delivered with an error means a removal was tried and could not be confirmed.
attemptsinteger
errorstring | null
error_codestring | nullA stable code beside error, to branch on.
delivered_atstring | null (date-time)
updated_atstring (date-time)

Budget

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

FieldTypeDescription
usedinteger
budgetinteger
remaininginteger

ActionResult

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

FieldTypeDescription
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, hold_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.

FieldTypeDescription
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 | timed_out | 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 per phone in the agency and 35,000 of ceiling per phone this key reaches, each plus a flat 2,500.

FieldTypeDescription
creditsBudgetThe only ceiling. Everything else here explains it.
included_creditsintegerToday's free allowance. It is the agency's, shared by every key and connection, so a new key does not start it over. Once agency_credits_used passes it, a call draws the part of its price above the line from credit_balance, or is refused credits_exhausted.
agency_credits_usedintegerWhat every key and connection of the agency has spent today, which is what included_credits is measured against.
paid_creditsintegerHow much of this key's spend today 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 ceiling, floored at 3. Zero leaves only the flat part. The included allowance is sized by every phone in the agency instead, so it does not move with this number. 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.

FieldTypeDescription
countinteger
creditsinteger

Group

FieldTypeDescription
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

FieldTypeDescription
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.

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
period *Period
members *MemberUsage[]

DeviceUsageReport

FieldTypeDescription
period *Period
devices *DeviceUsage[]
untouched *DeviceUsage[]Reachable phones a relay still reports that nobody touched in the period. The line item that costs money.

ActivityReport

FieldTypeDescription
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.

FieldTypeDescription
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

FieldTypeDescription
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.

FieldTypeDescription
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

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

Member

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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

FieldTypeDescription
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.

FieldTypeDescription
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 | vault_full | conflict
detail *string
request_idstringQuote this when contacting support.

Errors

Every failure is an RFC 7807 problem document. Branch on code, never on detail, which may be reworded.

CodeStatusMeaning
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. With Retry-After the phone was still carrying out an earlier command and nothing was sent, so wait that long and send it again. Without it the phone was reachable but slow and the command may still have landed, so check the screen before sending it again
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, which stops a key whatever phones it was given; create a replacement 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
vault_full409The media vault is full. Delete items you no longer need, or wait for old ones to expire, then upload again. GET /media reports usage
conflict409This clashes with something that already exists. detail names what clashed, usually a folder or tag name that is already taken