Claim and Route a Number
Claim and route a Kenyan number on Sautikit: search available numbers, claim one for KES 500 a month, set its callback URLs and test an incoming call.
Claim and route a Kenyan number on Sautikit: search available numbers, claim one for KES 500 a month, set its callback URLs and test an incoming call.
To claim and route a Kenyan number on Sautikit, search available numbers with the API, claim one (KES 500 a month, with the rest of the current month charged on claim), then set its voice_callback_url and events_url with PUT /v1/numbers/{id}/routing. Place a test call to confirm it reaches your server.
voice_callback_url. In development a tunnel such as ngrok or cloudflared works.curl -s "https://api.sautikit.com/v1/numbers/available?country=KE" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | jq '.available[0:3]'Each entry looks like this:
{
"inventory_id": "01900000-0000-7000-8000-000000000001",
"e164": "+254720000000",
"country": "KE",
"capabilities": ["voice"],
"monthly_price_minor": 50000,
"inbound_per_min_minor": 0,
"outbound_per_min_minor": 300,
"currency": "KES"
}Amounts are in minor units, so 50000 is KES 500 a month and 300 is KES 3.00 a minute. Pick a number and note its inventory_id.
curl -s -X POST "https://api.sautikit.com/v1/numbers/01900000-0000-7000-8000-000000000001/claim" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expected_monthly_minor": 50000}' | jq .expected_monthly_minor is optional. Send the monthly_price_minor you were shown and a price that has moved is refused with 409 numbers.price_changed and nothing is charged.
The API answers 201:
{
"number": {
"id": "01900000-0000-7000-8000-000000000002",
"e164": "+254720000000",
"status": "active"
},
"prorated_minor": 6667,
"monthly_minor": 50000,
"currency": "KES",
"next_renewal_at": "2026-07-01T00:00:00Z"
}prorated_minor is what the claim charged: the monthly price for the days left in this month. Store the number's id:
export NUMBER_ID="01900000-0000-7000-8000-000000000002"If the wallet cannot cover the charge, the API answers 402:
{ "error": { "code": "wallet.insufficient_funds", "message": "insufficient wallet balance" } }Top up by M-Pesa or card, then retry.
Two URLs connect the number to your server:
voice_callback_url: Sautikit POSTs here when a call arrives and at each later step. Your server replies with voice actions that control the call.events_url: Sautikit POSTs the call's events here as it rings, is answered and ends. These posts are not signed and not retried, so use them for logging and dashboards.Set them with PUT /v1/numbers/{id}/routing:
const res = await fetch(`https://api.sautikit.com/v1/numbers/${process.env.NUMBER_ID}/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",
}),
});
const { routing } = await res.json();
console.log(routing.voice_callback_url, routing.events_url);curl -s -X PUT "https://api.sautikit.com/v1/numbers/$NUMBER_ID/routing" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"voice_callback_url": "https://ivr.example.com/voice",
"events_url": "https://ivr.example.com/events"
}' | jq .routingBoth must be absolute URLs. Use https:// in production. A field you leave out keeps its current value, and an empty string clears it.
Sautikit POSTs JSON to your voice_callback_url and waits up to 60 seconds for your voice actions. callerNumber is the person calling and sessionId identifies the call.
import express from "express";
const app = express();
app.use(express.json());
app.post("/voice", (req, res) => {
const { callerNumber, sessionId, callSessionState } = req.body;
if (callSessionState === "Completed") return res.json({ actions: [] });
console.log(`Call ${sessionId} from ${callerNumber}`);
res.json({
actions: [
{ say: { text: "Hello, and thank you for calling." } },
{ hangup: {} },
],
});
});
app.listen(3000);from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/voice")
async def voice(request: Request):
body = await request.json()
if body.get("callSessionState") == "Completed":
return {"actions": []}
print("Call from", body.get("callerNumber"))
return {
"actions": [
{"say": {"text": "Hello, and thank you for calling."}},
{"hangup": {}},
]
}Each post to events_url is a JSON body describing where the call is: callSessionState moves through values such as Ringing, Answered and Completed, and the final event carries durationInSeconds.
app.post("/events", (req, res) => {
const { sessionId, callSessionState, callerNumber, durationInSeconds } = req.body;
console.log(sessionId, callSessionState, callerNumber, durationInSeconds ?? "");
res.sendStatus(200);
});Because these posts are unsigned, treat them as a prompt to read the call from the API (GET /v1/calls/{id} accepts the sessionId) before you act on anything that matters. Wallet, number and recording events are delivered as signed, retried webhooks: see Verify webhook signatures.
To register a softphone or desk phone on the number, create a SIP credential:
curl -s -X POST "https://api.sautikit.com/v1/numbers/$NUMBER_ID/sip-credentials" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "Reception desk phone"}' | jq .{
"id": "01900000-0000-7000-8000-000000000003",
"label": "Reception desk phone",
"username": "<generated>",
"password": "<generated>",
"host": "sip.sautikit.com",
"port": 5080,
"transport": "udp"
}Enter the username, password, host, port and transport in the phone. To see the password again later:
curl -s -X POST \
"https://api.sautikit.com/v1/numbers/$NUMBER_ID/sip-credentials/01900000-0000-7000-8000-000000000003/reveal" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | jq .passwordReveal changes nothing on the phone. To replace a password that has leaked, call /rotate on the same path and update the phone with the new one.
Call your number from any mobile phone. You should hear the greeting, and your server should log the callback and the events. Then read the call record:
curl -s "https://api.sautikit.com/v1/calls?direction=inbound&limit=1" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | \
jq '.data[0] | {direction, status, local_e164, remote_e164, duration_seconds}'Check the routing that is saved on the number:
curl -s "https://api.sautikit.com/v1/numbers/$NUMBER_ID" \
-H "Authorization: Bearer $SAUTIKIT_API_KEY" | \
jq '{e164, status, voice_callback_url, events_url}'Incoming calls are free. A call you forward on to a phone number is charged for the forwarded part at KES 3.00 a minute, billed per second.