What it costs. One cloud phone, billed monthly at the rate published on
Billing modes. Returning it at the end gives back the
part of the month you did not use, so a loop you finish in an afternoon costs a
fraction of a month rather than a month.What you need. A browser, an org admin sign in, and a card. Nothing
installed. About fifteen minutes.What you do not need. An API key, unless you want to do step 3.
Step 1: Buy a device
Go to/devices/add.
The screen draws itself from GET /v1/skus, which is the deployment’s actual
price list. One entry per pair of thing and billing mode, read on 2026-09-13:
amountMinor is the minor unit: 1900 with currency: "usd" means
nineteen dollars, not nineteen hundred. Prices are pre tax and come from the
payment provider’s own price objects, carried through unaltered; this service
does no arithmetic on a charge. The current rate card is on
Billing modes, and
phonebase.co is canonical if the two disagree.
A pair this deployment holds no price for is absent from the list rather
than shown without a number. So the list is exactly what can be bought here
today, and a card you cannot see is not a card you are missing.
stock says whether a free unit of that kind exists right now, and it has
three answers:
shipsNow is the older field for the same answer: true exactly when stock
is in_stock, so it cannot tell out_of_stock from unknown.
out_of_stock (and shipsNow: false) does not mean the purchase is
refused. By default it succeeds and the device is promised instead, which
GET /v1/fulfilments then reports as awaiting_provisioning with a deadline.
It is a verdict, never a count: how much stock is left is not published.POST /v1/checkout-sessions, which hands back somewhere to send the browser:
GET /v1/devices and on /devices. If you would rather be told than poll, the
fulfilment.changed webhook is the event for exactly this.
Step 2: Drive it from the console
Open/devices/:id for the device that appeared.
- Click the screen. The picture is the tap target. The console turns where you clicked into a coordinate on the normalized grid and sends the tap. Your first tap takes a lease on the device for ten minutes if your org holds none; if an agent’s run already holds one, your tap runs under it and the run keeps going.
- Keep clicking. A lease you took yourself stays alive while you drive.
- Give the device back when you are done, or let the lease expire.
Action not supported on this device. That is its capabilities block
speaking. The cloud phone used for this walkthrough reported tap, swipe,
typeText: "native", openApp: "intent" and installApp: "apk", and
pressKey: false.
Release the lease before you wander off. Nothing pauses the clock while a lease
is held, and nothing watches for idleness. A lease expires ten minutes after it
was granted and can be renewed, and no lease lives longer than sixty minutes
from when it was acquired, so the worst case is bounded but it is not free.
Step 3: Drive it from the API
Everything the console just did is three routes. Mint an API key at/keys
with three of the eight scopes: devices:read to see it, devices:lease to take
it, devices:act to touch it. Keys are for servers: an API key sent from a
browser origin is refused with api_key_from_browser_origin whatever its scopes
are.
Take the device
201, and the request takes no body at all. Keep the leaseId (export LEASE=...): it is a credential, it is returned by this call and by no other,
and every gesture and the release need it.
A refusal does not say why, and that is deliberate. Somebody else holding the
device, a spent quota and an unreachable device all answer the same way.
Look at the screen
200, image/jpeg. The measured run returned a 720 by 1280 frame of 52428
bytes. You do not need the lease for this one. Any credential in the org
that can see the device can watch it, whoever is holding it, which is the point:
a person should be able to see what a device is doing before deciding whether to
interrupt it.
Watching writes no receipt and returns no frameId. If you intend to bind an
action to the exact frame you decided on, take the screenshot through the MCP
screenshot tool instead, which issues one. See
Reading a trace.
Act
200. status has exactly one value: an action that did not commit is an
error, not a different status. The response resolves only once the device
applied the gesture, which is why it is not instant. One tap measured today took
5076 ms from request to committed.
Six gestures are available on this route, discriminated by action:
press_key takes one of home, back, enter, delete, app_switch,
power, volume_up, volume_down. Any of them can still be refused by a
device that declared pressKey: false, which is the whole reason to read
capabilities first rather than to assume a set.
Every attempt is written to the receipt ledger before it is dispatched, so
GET /v1/receipts can tell you what happened even if you lost this response.
Send your own idempotencyKey and a retry replays that outcome instead of
touching the phone twice. It is a field in the body, not a header, and there
is no Idempotency-Key header anywhere on this API. When you do not send one
the service generates it and returns it, which is why the response above carries
one you never chose.
Give it back
Referer headers. Releasing a device you do not hold does nothing,
so a retry is safe.
Working longer than ten minutes? PATCH /v1/devices/{deviceId}/lease with the
same leaseId slides the expiry to ten minutes from now. It does not add ten
minutes to the old one, and the leaseId does not change. Renew before it
expires: an expired lease cannot be renewed, because by then the device belongs
to whoever takes it next.
Step 4: Return the device
Back on/devices/:id, press Return device.
That is POST /v1/fulfilments/{id}/return, and it does three things at once: the
device goes back to the pool, your ownership of it ends, and billing for it
stops. It is admin only, like buying.
alreadyDone: truemeans the order had already ended before this call, so nothing was asked of the payment provider and no second refund was issued. Pressing the button twice answers200either way and is not an error.deviceIdnames the device that went back. It is non null on a return, where a device really changed hands, and null on a cancellation, where there was never a device to give back.leaseCutsays whether a session you were holding right then was cut. Whether it is depends on the deployment, not on this call. Either way the device stops being yours immediately.
409, and POST /v1/fulfilments/{id}/cancel is
the route for it. Cancelling an order that never arrived returns all of the
money.
The order afterwards is a different object
Read the order back fromGET /v1/fulfilments a few seconds later and it does
not look like the response you just got. Both readings are correct, and
comparing them field by field is a good way to confuse yourself:
deviceId is null here and non null in the return’s own response. That is not
a contradiction. The return response says which device went back. The
listing says which device this order currently holds, which is none, because
it went back.
state reads cancelled after a return, not returned. There is no
separate returned state. The direction is carried by revocationReason, which
is customer_returned when you gave a device back and customer_cancelled when
you cancelled an order that had not arrived.
At the same moment the sku goes back to shipsNow: true on GET /v1/skus. That
is the shelf side of the same event: the device you returned is the one the next
buyer gets.
What you were charged
- The device, monthly, at the published rate, less the unused remainder you got back on return.
- Active time is not a separate line on a monthly device. A monthly device has no meter: run it all month and the invoice reads the same. A per minute mode exists in the billing model, but no per minute pair is on the price list today, so a subscription is what you can actually buy. See Billing modes.
- Agent steps are metered but not separately priced. Steps are included in the device price today, and no separate step rate is published.
Where to go next
Webhooks
Be told when a run ends instead of polling for it.
Agent tier
Hand a goal to a worker instead of driving each step yourself.
The console
Every screen, and the four surfaces that have no screen.