Skip to main content
This is the shortest complete thing you can do with phonebase: get a device, make it do something, give it back. Everything below was run end to end with a real card on a real device. The numbers are readings, not illustrations.
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:
Three things to read off that. 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.
Press the buy button and the console starts a checkout with POST /v1/checkout-sessions, which hands back somewhere to send the browser:
The checkout is hosted by the payment provider. Promotion codes are accepted on it, so a code you were given goes in there rather than anywhere in this product. Pay, and the order settles on its own. You do not have to do anything to claim the device: the payment provider tells the control plane, the control plane grants the subscription and allocates a unit, and the device then appears in GET /v1/devices and on /devices. If you would rather be told than poll, the fulfilment.changed webhook is the event for exactly this.
Buying is admin only and session only. An API key cannot start a purchase and a member cannot commit the org to a subscription; both answer 403. An org that already has a subscription answers 409.

Step 2: Drive it from the console

Open /devices/:id for the device that appeared.
  1. 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.
  2. Keep clicking. A lease you took yourself stays alive while you drive.
  3. Give the device back when you are done, or let the lease expire.
Read the disabled controls as information rather than as breakage. A device that cannot press hardware keys draws that control disabled and says 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

The lease id goes in the body of a DELETE, which is unusual and deliberate: the id is a credential, and a query string is written to access logs, browser history and 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.
You get back the part you did not use, not the whole payment. You had the device and you used it. The amount is computed by the payment provider from its own prices; this service states no amount here or anywhere, because an amount computed on this side would be a second opinion about a price it deliberately cannot read. The measured run ended with the invoice refunded and the subscription cancelled at the provider. Three fields worth knowing:
  • alreadyDone: true means 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 answers 200 either way and is not an error.
  • deviceId names 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.
  • leaseCut says 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.
An order still awaiting provisioning cannot be returned, because there is nothing to give back. That is 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 from GET /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.
A null revocationReason on a cancelled row means not recorded, never a reason of its own. Do not render it as one.
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.
There is no spend cap. No dollar limit and no minute limit: nothing suspends a lease or refuses the next one. There is an optional alert that emails you when the next invoice passes a level, and it only tells you. The limits that actually stop something are the lease expiry and a run’s own step budget. See Spending controls.

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.