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_7k2m9qstops the renewal; the phone runs to the end of the paid 30 days.