docs / quickstart Markdown

Quickstart

Five calls: check the catalog, buy a phone, wait for it, look at the screen, tap. Then hand the phone to your agent over MCP.

1. Get a key

Sign in at fones.ai/login with your email, top up the balance (from $10) and create an API key under Developers. The key is shown once. Keep it in an environment variable:

export FONES_API_KEY=fones_...   # from Developers, API keys
export API=https://api.fones.ai/v1

New keys have every scope: phones:read, phones:control and phones:buy. Create a key without phones:buy for an agent that should never spend.

2. Check the catalog

curl $API/catalog -H "Authorization: Bearer $FONES_API_KEY"

The answer lists the tiers (cloud, identity), the models you can pick for a real phone identity, the prices in cents and your balance.

3. Buy a phone

curl -X POST $API/phones \
  -H "Authorization: Bearer $FONES_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: first-phone" \
  -d '{"tier":"cloud","number":true,"label":"first"}'
{
  "id": "ph_7k2m9q",
  "label": "first",
  "tier": "cloud",
  "status": "provisioning",
  "number": null,
  "number_status": "provisioning",
  "ip": null,
  "proxy_status": null,
  "monthly_cents": 2800
}

The first month ($19 for the phone, $9 for the number) comes from the balance now. Send the same Idempotency-Key again and you get the same phone, never a second one. The phone starts as provisioning.

4. Wait until it is ready

curl $API/phones/ph_7k2m9q -H "Authorization: Bearer $FONES_API_KEY"

When status is ready, the phone has its number and, if you asked, its static IP (one that could not be set up is refunded instead, with number.failed or proxy.failed: the phone is ready all the same). With a webhook you get phone.ready instead of polling. If setup fails you get phone.failed and the charge back.

5. Look, then act

curl $API/phones/ph_7k2m9q/screenshot -H "Authorization: Bearer $FONES_API_KEY" -o screen.png
curl $API/phones/ph_7k2m9q/ui -H "Authorization: Bearer $FONES_API_KEY"

ui returns the screen as nodes: text, id, class, whether it is clickable, and its center on the 0 to 1000 grid. Tap one:

curl -X POST $API/phones/ph_7k2m9q/actions \
  -H "Authorization: Bearer $FONES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"tap","x":500,"y":418}'

That is the whole loop: look, act, look again. In code:

const api = (path: string, init: RequestInit = {}) =>
  fetch(`https://api.fones.ai/v1${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${process.env.FONES_API_KEY}`, 'Content-Type': 'application/json' },
  }).then((r) => r.json());

const phone = 'ph_7k2m9q';
const { nodes } = await api(`/phones/${phone}/ui`);          // look
const button = nodes.find((n) => n.text === 'Send code');
await api(`/phones/${phone}/actions`, {                        // act
  method: 'POST',
  body: JSON.stringify({ type: 'tap', x: button.x, y: button.y }),
});
const sms = await api(`/phones/${phone}/sms`);                // look again
console.log(sms.data[0]?.code);
import os, requests

api = "https://api.fones.ai/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['FONES_API_KEY']}"

phone = "ph_7k2m9q"
nodes = s.get(f"{api}/phones/{phone}/ui").json()["nodes"]          # look
button = next(n for n in nodes if n["text"] == "Send code")
s.post(f"{api}/phones/{phone}/actions",                            # act
       json={"type": "tap", "x": button["x"], "y": button["y"]})
sms = s.get(f"{api}/phones/{phone}/sms").json()["data"]           # look again
print(sms[0]["code"] if sms else None)

Next

  • Connect your agent and let it drive the phone itself.
  • MCP tools and the REST API for every action.
  • When you are done, DELETE /v1/phones/ph_7k2m9q stops the renewal; the phone runs to the end of the paid 30 days.