---
title: 'From zero to hello: place your first call in 5 minutes'
description: >-
  Sign up, fund your wallet, claim a number, create an API key, and POST to
  /v1/calls. Every step has a real curl snippet you can run.
summary: >-
  A concrete walkthrough of signup, wallet top-up, number claim, API key
  creation, and placing a call with curl: from zero to hello in under 5 minutes.
date: 2026-06-27T00:00:00.000Z
type: blog
---


## Summary

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.

## What you need

- A Sautikit account (takes under 2 minutes to create)
- KES in your wallet: minimum KES 100 (ex. VAT; KES 116 incl. VAT) to cover one month's number rental
- A phone number to call (your own mobile works fine for testing)
- `curl` on your local machine

The API base URL is `https://api.sautikit.com`. All examples below assume this base URL.

## Step 1: Sign up and complete onboarding

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`:

```bash
# 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.

## Step 2: Fund your wallet

Sautikit runs a prepaid wallet. Calls are deducted in real time as they complete. You need a balance before anything can happen.

Check your current balance:

```bash
curl -s https://api.sautikit.com/v1/wallet/balance \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

```json
{
  "balance_minor": 0,
  "currency": "KES",
  "status": "healthy",
  "server_time": "2026-06-27T09:00:00Z"
}
```

`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`.

## Step 3: Search for and claim a number

Search the available inventory for numbers in your market (Kenya today):

```bash
curl -s "https://api.sautikit.com/v1/numbers/available?country=KE" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

```json
{
  "available": [
    {
      "inventory_id": "a1b2c3d4-...",
      "e164": "+254712345678",
      "country": "KE",
      "capabilities": ["voice"],
      "monthly_price_minor": 10000,
      "currency": "KES",
      "inbound_per_min_minor": 0,
      "outbound_per_min_minor": 300
    }
  ]
}
```

`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).

Pick an `inventory_id` and claim it:

```bash
curl -s -X POST \
  "https://api.sautikit.com/v1/numbers/a1b2c3d4-.../claim" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

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`.

Confirm it is in your workspace:

```bash
curl -s https://api.sautikit.com/v1/numbers \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

## Step 4: Create an API key

Session cookies work in the browser dashboard. For programmatic access, create a JWT API key:

```bash
curl -s -X POST https://api.sautikit.com/v1/api-keys \
  -H "Content-Type: application/json" \
  -b "sk_session=<your-session-cookie>" \
  -d '{
    "label": "dev-test",
    "scopes": ["calls.write", "numbers.read"]
  }'
```

The response includes a `jwt` field. This is the only time you see the plaintext JWT, so copy it now. It is not stored server-side in recoverable form.

```json
{
  "id": "...",
  "label": "dev-test",
  "prefix": "sk_k_abc",
  "scopes": ["calls.write", "numbers.read"],
  "status": "active",
  "jwt": "eyJ...",
  "workspace_id": "..."
}
```

Set it as an environment variable:

```bash
export SAUTIKIT_API_KEY="eyJ..."
```

To revoke a key later: `DELETE /v1/api-keys/{id}`. The key is invalidated immediately via a deny-list checked on every request.

## Step 5: Place the call

You have a funded wallet, a claimed number, and an API key. Place the call:

```bash
curl -s -X POST https://api.sautikit.com/v1/calls \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "from": "+254712345678",
    "to": ["+254700000001"]
  }'
```

- `from` must be your claimed number in E.164 format.
- `to` is an array of E.164 destination numbers. The first element is dialled.
- The `Idempotency-Key` header prevents duplicate dials if you retry a failed request.

Response (202: call accepted, dialling in progress):

```json
{
  "call_id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",
  "pbx_session_id": "HD_1a2b3c",
  "status": "ringing",
  "events_url": "/v1/calls/9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0/events"
}
```

The phone at `+254700000001` is now ringing. The 202 response arrives before the callee picks up. The call progresses asynchronously.

## Checking call status

Poll the call record to see what happened:

```bash
curl -s "https://api.sautikit.com/v1/calls/9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

```json
{
  "id": "9d2b1f53-8c0e-4f1d-9a6b-5d3a8c47e9f0",
  "direction": "outbound",
  "local_e164": "+254712345678",
  "remote_e164": "+254700000001",
  "status": "completed",
  "duration_seconds": 47,
  "cost_minor": 235,
  "cost_currency": "KES"
}
```

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`.

## The same flow in JavaScript

The curl steps above map directly to JavaScript's global `fetch`. Here is the complete equivalent in Node.js 18+.

**Top up your wallet:**

```js
await fetch("https://api.sautikit.com/v1/topups", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ provider: "mpesa", amount: 200, phone: "+254700000000" }),
});
```

**Place the call:**

```js
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):**

```js
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:

```xml
<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>
```

```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" } }
  ]
}
```

Note: `<GetDigits>` nests its prompt `<Say>` inside the element, and XML uses `maxDigits` where JSON uses `numDigits`.

**Track the outcome with a webhook:**

```js
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
});
```

> 
> 
> Acknowledge webhooks with a `200` quickly. Do slow work (DB writes, downstream calls) after responding, or move it to a queue; otherwise delivery retries can pile up.
> 
> 

## Error cases to know

**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.

## Get started

1. [Create a Sautikit workspace](/) and claim a phone number.
2. Top up over **M-Pesa**: KES billing, no card.
3. Create an API key and POST your first call to `/v1/calls`.

**[Start with Sautikit →](/)** &nbsp;·&nbsp; **[See pricing →](/pricing)** &nbsp;·&nbsp; **[Need SMS, WhatsApp & an agent desk? Helloduty →](https://helloduty.com)**

Need to reach people over SMS, WhatsApp, or USSD, or add a human-agent desk alongside voice? [Helloduty](https://helloduty.com) is the multi-channel home for that.

## Next steps

- [How the call stack composes: numbers, SIP, voice actions, recordings](/blog/the-call-stack)
