An AI agent can buy a Kenyan phone number and point it at your IVR through Sautikit's hosted MCP server, in four tool calls: search_available_numbers, claim_number, validate_voice_actions and update_number_routing. The number costs KES 500 a month. Four limits keep the agent safe to run: scoped API keys, a prepaid wallet it cannot refill, a confirmation prompt on every tool that spends money, and idempotency keys.
Idempotency keys mean a retried claim cannot buy two numbers. All four limits are enforced by the API, so they hold whatever the prompt says.
The whole flow is one instruction to Claude, through the claude.ai connector or Claude Code, against the hosted MCP server at mcp.sautikit.com/mcp (setup guide):
search_available_numbers lists Kenyan numbers you can claim. Each row carries its monthly price and per-minute rates, so the agent can tell you the cost before committing ("this one is KES 500 a month and incoming calls are free").
claim_number is the step that spends money, and it is where the agent stops. The tool is marked as money-spending, so your MCP client asks you to confirm before anything is charged. When you approve, Sautikit takes the prorated first month from your prepaid wallet and returns the number with its renewal date. There is no setup fee.
validate_voice_actions checks the IVR's response document against the JSON schema Sautikit enforces on live calls. It costs nothing and needs no scope. Language models tend to invent plausible field names; this catches them before a real call does.
update_number_routing points the number's voice_callback_url at your handler and events_url at your event receiver. Calls to the new number now reach the IVR.
The document validated in step 3 is ordinary voice-actions JSON, for example a one-level menu:
{ "actions": [ { "getDigits": { "timeout": 8, "numDigits": 1, "nested": [ { "say": { "text": "Thanks for calling. Press 1 for sales, 2 for support." } } ] } }, { "say": { "text": "We did not receive a selection. Goodbye." } }, { "hangup": {} } ]}
That is four tool calls and one confirmation from you. If the agent later gives the number up, release_number is marked destructive, so that stops for confirmation too.
Every MCP tool wraps the public REST API: the same endpoints, auth, scopes, rate limits and billing your own code uses. These are the three network calls underneath the conversation:
// 1. Search inventoryconst search = await fetch( "https://api.sautikit.com/v1/numbers/available?country=KE&limit=5", { headers: { Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}` } },);const { available } = await search.json(); // rows carry monthly and per-minute rates in minor units// 2. Claim. This is the step that spends money.// The Idempotency-Key makes retries safe (more below).const inventoryId = "..."; // id of the chosen row from the search responseconst claim = await fetch( `https://api.sautikit.com/v1/numbers/${inventoryId}/claim`, { method: "POST", headers: { Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`, "Idempotency-Key": "provision:support-line:2026-09", }, },);// 201: { number, prorated_minor, monthly_minor, currency, next_renewal_at }// 402: wallet.insufficient_funds (top up first)// 409: another workspace claimed this number first// 3. Route itawait fetch(`https://api.sautikit.com/v1/numbers/${numberId}/routing`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ voice_callback_url: "https://ivr.example.com/voice", events_url: "https://ivr.example.com/events", }),});
The routing endpoint behaves like a patch: fields you leave out are untouched, an empty string clears a field, and a malformed URL is rejected with 400 numbers.invalid_routing_url, an error the agent can read and correct on its next turn.
Everything an agent can buy on Sautikit is paid from the prepaid, KES-denominated wallet. A claim with too little balance fails with 402 wallet.insufficient_funds. New calls are refused at a zero balance, and live calls are ended when the balance runs out. So if you fund an agent's workspace with KES 2,000, a bug, a loop or a prompt injection can spend about KES 2,000 and then it stops. Nothing is billed after the fact.
Two design choices support this:
Topping up is not a tool. Top-ups, API-key creation and secret rotation are left out of the MCP tool set. An agent can spend its wallet down, but it cannot refill it or give itself wider access. You top up yourself, by M-Pesa or card.
The agent can see its own budget.get_wallet_balance costs nothing, so a well-prompted agent checks the balance before proposing a purchase. You can also subscribe to the wallet.low_balance webhook as a separate alarm; Sautikit sends it, and an email, at most once every 24 hours when the balance is at or below your threshold.
People sign in with OAuth. Add Sautikit as a custom connector on claude.ai, sign in, pick a workspace and approve the scope groups. No API key leaves the console, and you can revoke the grant at any time under Settings, Connected apps.
Autonomous agents use scoped API keys. A headless agent, such as Claude Code in CI or a scheduled provisioning job, authenticates with a key you create, and the key should carry only the scopes the job needs. For a number-provisioning agent that is numbers.read, numbers.claim and wallet.read. The MCP server only lists the tools a key's scopes allow, so an agent whose key lacks broadcasts.manage never sees start_broadcast. A revoked key stops working on its next request.
Scopes decide what an agent may do. Confirmation decides what it does right now. Sautikit marks every money-spending tool (claim_number, place_call, start_broadcast, send_agent_call) so MCP clients ask the person before running it, and marks destructive tools such as release_number the same way. Claude shows these as approval prompts with allow-once and always-allow choices.
Start every new agent on "confirm everything that spends", and move individual tools to always-allow only after you have watched the agent work.
The common failure in agent systems is the retry. An agent calls claim_number, the response times out, and the agent tries again. Without protection you would own two phone numbers.
The claim endpoint takes an Idempotency-Key header of up to 64 characters. If you are building your own agent loop against the REST API, always send one, and build it from what the purchase is for (provision:support-line:2026-09), not from a random value, so every retry of the same intent carries the same key.
A key is remembered for 24 hours, per workspace, HTTP method and path.
A repeat within that time returns the original status and body with the header X-Idempotency-Replayed: true. The agent gets the number it already bought.
Responses from 2xx to 4xx are stored. A 5xx is never stored, so retrying after a server error runs the request again.
Reusing a key with a different body returns idempotency.conflict. Reuse the original body or send a new key.
The 409 in the claim flow is different: it means another workspace claimed the same number first. An agent should treat that 409 as "search again and pick another number" and a replayed 201 as "already done".
The Sautikit MCP server is hosted, signs you in with OAuth and has more than 80 tools, including live call control. The full list is on the MCP page.
Create a workspace, top up by M-Pesa or card (the balance is your agent's spend limit), connect the MCP server and ask your agent for a Kenyan number. You can also browse numbers yourself.