Phone Numbers
How Sautikit numbers work: browse inventory, claim a Kenyan number for KES 500 a month, add SIP credentials, set call routing, and release it.
How Sautikit numbers work: browse inventory, claim a Kenyan number for KES 500 a month, add SIP credentials, set call routing, and release it.
A Sautikit number is a Kenyan (+254) number you claim into your workspace for KES 500 a month, with no setup fee. Each number has its own routing (where Sautikit sends its calls) and its own SIP credentials (for desk phones, softphones and office PBXs).
Two different things get called a "SIP trunk" and they are set up differently:
| What it is | Cost | |
|---|---|---|
| SIP trunk for your PBX | Your PBX or phone signs in to Sautikit with a SIP credential and uses a Sautikit number | KES 500 a month for the number |
| Bring your own carrier | You connect your own carrier's trunk and the numbers you already own | KES 2,000 a month for the trunk, no number rent |
The JavaScript examples below share these headers:
const headers = {
Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
"Content-Type": "application/json",
};const res = await fetch(
"https://api.sautikit.com/v1/numbers/available?country=KE",
{ headers }
);
const { available } = await res.json();Each row has an inventory_id, the number in e164, monthly_price_minor (KES in minor units; divide by 100 for shillings), first_charge_net_minor (what claiming today costs), inbound_per_min_minor and outbound_per_min_minor.
Claiming charges the rest of the current month's rental from your wallet and adds the number to your workspace. It is live in seconds.
const res = await fetch(
`https://api.sautikit.com/v1/numbers/${inventoryId}/claim`,
{ method: "POST", headers }
);
const number = await res.json();If the wallet balance is too low the API returns 402 wallet.insufficient_funds. Top up by M-Pesa or card and try again.
| Status | Meaning |
|---|---|
active | In use |
suspended | Suspended by Sautikit support |
released | No longer yours; the number is back in the available pool |
A SIP credential lets your own equipment (a desk phone, an office PBX, Odoo, a softphone) sign in to Sautikit and place or receive calls on the number. This is how you use a Sautikit number as a SIP trunk for your PBX; see Use a Sautikit number as a SIP trunk for your PBX.
const res = await fetch(
`https://api.sautikit.com/v1/numbers/${numberId}/sip-credentials`,
{
method: "POST",
headers,
body: JSON.stringify({ label: "Reception desk phone" }),
}
);
const credential = await res.json();Sautikit generates the username and password and returns everything the device needs:
{
"id": "…",
"label": "Reception desk phone",
"username": "…",
"password": "…",
"host": "sip.sautikit.com",
"port": 5080,
"transport": "udp",
"realm": "sip.sautikit.com"
}Enter host, port, transport, realm, username and password on the device. The server is sip.sautikit.com, port 5080, UDP.
A number can hold several credentials. Create one per device so each has its own password, and label them ("Reception desk phone", "Odoo prod") so you can tell them apart. Revoking one leaves the others working.
Each number has a limit on active credentials. The list response reports active_count and limit, and creating past the limit returns numbers.credential_limit_reached. Revoking a credential frees a slot.
Give a credential a four-digit extension and your other registered devices reach it by dialling those digits. Reception dials 1001 and the handset in the back office rings.
await fetch(
`https://api.sautikit.com/v1/numbers/${numberId}/sip-credentials`,
{
method: "POST",
headers,
body: JSON.stringify({ label: "Back office", extension: "1001" }),
}
);Calls between two of your devices by extension are free and do not appear in your call log.
1001 means one device, whichever of your numbers the credential belongs to. A clash returns numbers.extension_taken.numbers.extension_invalid.Changing an extension applies immediately and needs no change on the device. Pass an empty string to remove it. Revoking a credential frees its extension for reuse.
An extension is also a valid forward destination, so an outside call can ring a desk phone (see Configure routing). That call is logged and billed as a normal call.
If the device is switched off when someone dials its extension, the caller hears a short message.
You can show a credential's password again at any time. Nothing is changed and a registered device keeps working:
const res = await fetch(
`https://api.sautikit.com/v1/numbers/${numberId}/sip-credentials/${credentialId}/reveal`,
{ method: "POST", headers }
);
const { password } = await res.json();POST …/sip-credentials/{credential_id}/rotate replaces the password. The username stays the same, but any device using the old password is disconnected until you enter the new one.
A credential is ready to register as soon as the create call returns 201. If it cannot be set up, the request fails with numbers.sip_provisioning_failed and nothing is created.
| Status | Meaning |
|---|---|
active | Can register |
revoked | Permanently disabled; the device is disconnected immediately |
Routing tells Sautikit where to send a number's calls and call events.
await fetch(`https://api.sautikit.com/v1/numbers/${numberId}/routing`, {
method: "PUT",
headers,
body: JSON.stringify({
voice_callback_url: "https://ivr.example.com/voice",
events_url: "https://ivr.example.com/events",
}),
});curl -X PUT "https://api.sautikit.com/v1/numbers/{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"
}'voice_callback_url: Sautikit POSTs here on each call step. Your server returns voice actions.events_url: Sautikit POSTs call events here (call.started, call.answered, call.completed, call.failed, call.recording.ready). These are per number; they are not workspace webhooks.Fields you leave out are unchanged. Send an empty string to clear a URL. The same request also takes forward, voicemail, queue, hours and inbound_agent; see the routing API reference.
Use https:// URLs in production.
A number costs KES 500 a month. The first charge, at claim, covers the rest of that month. After that the rental is charged from your wallet each month as a number_rental ledger entry.
If the wallet cannot cover a renewal, Sautikit sends a number.rental_past_due webhook and an email, and retries the charge every 6 hours. If the rental is still unpaid after 3 days the number is released.
Trial numbers issued by Sautikit support are released when the trial ends. A reminder email is sent 24 hours before.
await fetch(`https://api.sautikit.com/v1/numbers/${numberId}`, {
method: "DELETE",
headers,
});Billing stops on release. The number goes back to the available pool and another workspace may claim it.
Everything above is about a number rented from Sautikit. You can also bring your own carrier: connect the trunk your carrier gave you, add the numbers you already own, and handle their calls with Sautikit.
This is different from using a Sautikit number as a SIP trunk for your PBX. There, your PBX signs in to Sautikit. Here, Sautikit connects to your carrier.
A workspace can rent numbers from Sautikit and bring its own carrier at the same time. A rented number pays its monthly rental and Sautikit's call rates. A number on your carrier trunk has no rental and is charged at your trunk's rates. Calls placed from one of your own numbers leave through your carrier, and calls placed from a Sautikit number leave through Sautikit.
A workspace connects one carrier trunk. POST /v1/sip-trunk connects it and PUT /v1/sip-trunk changes it; there is no trunk ID in the URL. Every number you bring uses that trunk.
const res = await fetch("https://api.sautikit.com/v1/sip-trunk", {
method: "PUT",
headers,
body: JSON.stringify({
label: "Head office trunk",
auth_mode: "registration",
proxy: "sip.example-carrier.com",
transport: "udp",
username: "sautikit_100",
password: "…",
}),
});
const trunk = await res.json();
console.log(trunk.status); // "active", "pending", or "failed"Which one you use depends on how your carrier identifies you.
Your carrier gave you a SIP username and password. Sautikit registers with your carrier using them, the way a desk phone would.
The request waits for your carrier to confirm the registration. status: "active" means it registered. status: "failed" (with last_error set) means the carrier rejected it or could not be reached, most often because of a mistyped username or password.
Your carrier identifies you by IP address. Each side allowlists the other, and you submit two sets of carrier addresses:
signaling_ips: where your carrier's SIP messages (call setup) come from.media_ips: where your carrier's call audio (RTP) comes from.If your carrier uses one address for both, put it in both fields. A request without media_ips is rejected with sip_trunk.invalid.
const res = await fetch("https://api.sautikit.com/v1/sip-trunk", {
method: "PUT",
headers,
body: JSON.stringify({
label: "Carrier direct",
auth_mode: "ip_allowlist",
proxy: "sip.example-carrier.com",
transport: "udp",
username: "sautikit_101",
country_code: "+254",
signaling_ips: ["203.0.113.10", "203.0.113.0/24"],
media_ips: ["203.0.113.10", "203.0.113.0/24"],
}),
});An IP trunk needs approval from Sautikit before it goes live. It stays at status: "pending_approval" until then.
A trunk that is not active (wrong password, unreachable carrier, still awaiting approval) costs nothing and you can correct it and try again.
If a monthly charge leaves your wallet below zero and it is still below zero 72 hours later, the trunk is paused. Calls stop, but the trunk and its numbers are kept. Top up and it switches back on within the hour.
Once the trunk is active, add a number:
const res = await fetch("https://api.sautikit.com/v1/numbers/byo", {
method: "POST",
headers,
body: JSON.stringify({
e164: "+254720000000",
friendly_name: "Head office main line",
}),
});
const number = await res.json();This fails with numbers.byo_requires_trunk if the trunk is not active yet, and numbers.e164_taken if the number is already on Sautikit.