This is a walkthrough from nothing to a ringing phone call. It covers: create an account, top up your wallet, claim a phone number, create an API key, and POST to /v1/calls. Each step has a curl command you can run directly. Total time is under 5 minutes if your wallet is already funded.
Go to sautikit.com and sign up with your email. Sautikit sends a magic link; there is no password to set. Click it and you land on the onboarding screen.
Onboarding creates your first workspace. You provide your name, a workspace name, your country (KE for Kenya), and your billing currency (KES). The currency is locked after creation, since it determines how your wallet is denominated.
The API route that backs this is POST /v1/auth/onboarding:
# This is what the dashboard calls after magic-link completion.# You don't need to call it directly; the dashboard UI handles it.curl -s -X POST https://api.sautikit.com/v1/auth/onboarding \ -H "Content-Type: application/json" \ -b "sk_session=<your-session-cookie>" \ -d '{ "name": "Alice Kamau", "workspace_name": "Acme Fintech", "country": "KE", "currency": "KES" }'
The response gives you a workspace_id. Keep it handy.
balance_minor is in KES cents. 0 means an empty wallet.
To top up, initiate an M-Pesa STK push from the dashboard under Wallet → Top Up. The STK push flow sends a prompt to your registered phone within seconds. Your workspace's stable M-Pesa account reference is shown in the top-up modal; use it as the AccountReference when doing a manual Paybill payment instead.
Once funds land, the wallet.top_up webhook event fires and your GET /v1/wallet/balance will show the new balance. A KES 1,000 top-up arrives as balance_minor: 100000.
monthly_price_minor: 10000 is KES 100 (ex. VAT). inbound_per_min_minor: 0 is KES 0 (inbound is free). outbound_per_min_minor: 300 is KES 3.00 (KES 0.05/sec).
The claim endpoint debits a prorated number rental from your wallet for the remainder of the current billing month. On success you get back a TenantNumber object with your new number's id, e164, and status: active.
A 47-second call at KES 0.05/sec costs cost_minor: 235 = KES 2.35, billed per second from the moment the call connects, not rounded up to a full minute. That deduction appears in GET /v1/wallet/balance and GET /v1/wallet/statements.
const res = await fetch("https://api.sautikit.com/v1/calls", { method: "POST", headers: { Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ from: "+254200000000", // your claimed number to: ["+254700000000"], // who to call }),});const call = await res.json();// 202 { call_id, pbx_session_id: "HD_...", status: "ringing", stream_url }
Handle the answer with a voice callback (a mini IVR):
import express from "express";const app = express();app.use(express.urlencoded({ extended: false }));app.post("/voice", (req, res) => { res.json({ actions: [ { say: { text: "Hi from Sautikit. Press 1 for sales, or 2 for support.", language: "en-KE", }, }, { getDigits: { numDigits: 1, timeout: 8, finishOnKey: "#", action: "/voice/handle-choice", }, }, ], });});app.post("/voice/handle-choice", (req, res) => { const choice = req.body.Digits; if (choice === "1") { return res.json({ actions: [{ say: { text: "Connecting you to sales.", language: "en-KE" } }], }); } res.json({ actions: [{ say: { text: "Connecting you to support.", language: "en-KE" } }], });});app.listen(3000, () => console.log("voice callback on :3000"));
If you are migrating from an XML-based voice API (Africa's Talking, Twilio/TwiML), the same greet-and-collect step reads naturally as XML. Both forms below work at runtime: return JSON or XML, whichever fits your codebase. Sautikit parses and validates the JSON DSL; raw XML is forwarded to the telephony engine byte-for-byte:
{"actions": [ { "say": { "text": "Hi from Sautikit. Press 1 for sales, or 2 for support.", "language": "en-KE" } }, { "getDigits": { "numDigits": 1, "timeout": 8, "finishOnKey": "#", "action": "/voice/handle-choice" } }]}
<Response><Say language="en-KE">Hi from Sautikit. Press 1 for sales, or 2 for support.</Say><GetDigits maxDigits="1" timeout="8" finishOnKey="#"> <Say language="en-KE">Enter your choice.</Say></GetDigits></Response>
Note: <GetDigits> nests its prompt <Say> inside the element, and XML uses maxDigits where JSON uses numDigits.
Track the outcome with a webhook:
app.post("/webhooks/sautikit", (req, res) => { const { event, data } = req.body; if (event === "call.completed") { console.log(`call ${data.call_id} ended: ${data.status}`); // persist the outcome, reconcile the wallet charge, etc. } res.sendStatus(200); // ack fast so retries don't pile up});
402 wallet.insufficient_balance: your wallet is at zero. Top up and retry. The PBX was never contacted so no leg was billed.
400 validation.bad_request: from not found in your workspace, or to is empty. Double-check the number is claimed and in E.164 format including the + prefix.
401: your API key is expired or revoked. Create a new one.