sautikit
PricingDevelopersBlogAbout
Sign inStart building
Guides
  • Browser Calling with WebRTC
  • Build a Call Center with Conferences
  • Build a Voice IVR
  • Claim and Route a Number
  • Quickstart: Place a Call
  • Record and Stream to S3
  • Verify Webhook Signatures
Concepts
  • Browser Calling with WebRTC
  • Calls
  • Phone Numbers
  • Voice Actions DSL
  • Wallet and Billing
  • Webhooks
  • Workspaces
Voice Actions
  • Conference
  • Dial
  • GetDigits
  • Hangup
  • Play
  • Record
  • Redirect
  • Reject
  • Say
Webhooks
  • call.answered
  • call.completed
  • call.failed
  • call.recording.ready
  • call.started
  • number.provisioned
  • number.released
  • storage.tier_changed
  • wallet.low_balance
  • wallet.top_up
SDKs
  • Go SDK
  • Node.js SDK
  • PHP SDK
  • Python SDK
Errors
  • account.suspended
  • accounts.admin_required
  • accounts.last_owner
  • accounts.remove_denied
  • accounts.role_change_denied
  • accounts.write_denied
  • api_key.already_revoked
  • api_key.expired
  • api_key.invalid
  • api_key.name_required
  • api_key.not_found
  • api_key.revoked
  • api_key.scope_denied
  • api_key.scope_invalid
  • audit.invalid_action_kind
  • auth.admin_required
  • auth.cannot_unlink_only_auth
  • auth.email_taken
  • auth.google_exchange_failed
  • auth.google_not_configured
  • auth.google_state_mismatch
  • auth.invite_email_mismatch
  • auth.no_workspace
  • auth.session_invalid
  • auth.token_expired
  • auth.token_invalid
  • auth.token_used
  • auth.workspace_forbidden
  • calls.events_failed
  • calls.get_failed
  • calls.list_failed
  • calls.not_found
  • calls.recording_expired
  • calls.recording_failed
  • calls.recording_not_found
  • calls.stats_failed
  • currency.unsupported
  • idempotency.conflict
  • internal_error
  • invitations.not_found
  • invite_requires_login
  • method_not_allowed
  • mpesa.unavailable
  • not_found
  • numbers.already_claimed
  • numbers.invalid_routing_url
  • numbers.not_assigned
  • numbers.not_found
  • numbers.price_missing
  • numbers.retired
  • numbers.series_invalid
  • numbers.series_not_active
  • numbers.sip_config_unavailable
  • numbers.sip_credentials.already_revoked
  • numbers.sip_credentials.not_found
  • numbers.sip_token_unavailable
  • numbers.suffix_collision
  • numbers.wallet_unavailable
  • paystack.unavailable
  • pbx.body_read_failed
  • pbx.ingest_failed
  • pbx.payload_decode_failed
  • pbx.resolve_failed
  • pbx.unknown_workspace
  • provider.has_active_series
  • provider.invalid_credentials
  • storage.tier_invalid
  • storage.tier_switch_failed
  • topup.not_found
  • validation.bad_request
  • validation.invalid_cursor
  • wallet.adjustment_invalid_kind
  • wallet.alert_subscriber_exists
  • wallet.alert_subscriber_invalid_email
  • wallet.alert_subscriber_not_found
  • wallet.alert_subscribers_max
  • wallet.currency_mismatch
  • wallet.insufficient_funds
  • wallet.invalid_amount
  • wallet.not_found
  • wallet.topup_below_minimum
  • webhooks.create_failed
  • webhooks.delete_failed
  • webhooks.deliver.secret_unset
  • webhooks.delivery_not_retryable
  • webhooks.dispatcher_unavailable
  • webhooks.event_unsupported
  • webhooks.list_failed
  • webhooks.not_found
  • webhooks.patch_failed
  • webhooks.rotate_secret_failed
Changelog
  • 2026-06-15: Public Pricing Endpoint
  • 2026-06-20: WebRTC SIP Token (HS256 5-min TTL + Key Rotation)
  • 2026-06-27: sautikit.com Launch
  • Inbound voice is now free; outbound drops to KES 3/min

Calls

How Sautikit models call legs, states, directions, inbound vs outbound flow, and recording lifecycle.

2026-06-27

Next Steps

  • Quickstart: Place a CallFive steps from zero to a live outbound call on Sautikit. Sign up, add KES credit, claim a Kenyan DID, POST to /v1/calls/originate, and verify the signed webhook payload.
sautikit

Programmable voice infrastructure for Africa. Buy numbers, place calls, and bill per second, all in KES, via API.

Product

NumbersCalls & routingRecordingsWallet & billingPricing

Developers

DocumentationAPI referenceQuickstartAI promptChangelog

Company

AboutBlogCareersConsole

© 2026 Sautikit. All rights reserved.

Sautikit provides voice API services for application developers. Numbers provisioned on this platform are not configured for emergency calling (e.g. 999 / 112). Do not use Sautikit numbers as a replacement for a primary phone line.

Summary

Every phone call in Sautikit is a calls record with a direction (inbound or outbound), a sequence of lifecycle states, and an optional recording. Call cost is calculated at hangup from the answered duration and debited from your KES wallet in a single ledger entry.

Call directions

DirectionDescription
inboundA caller dials your Sautikit number; Sautikit routes it to your voice_callback_url
outboundYour application calls POST /v1/calls/originate; Sautikit dials the remote party

For inbound calls Sautikit POSTs the call state to your voice_callback_url and your server responds with VoiceAction DSL verbs that control the call flow.

Call states

A call moves through states in order. Not every state is visited on every call.

Call state machine
Call state machineA call begins in the ringing state. It transitions to answered (and then to completed) or directly to no_answer. Both completed and no_answer are terminal states.answered or notringingansweredno_answerterminalcompletedterminal← call is dialling+ failed+ busy+ canceled
completed is the only fully-successful terminal state. failed · busy · canceled are additional terminal states not shown above.
StatusMeaning
ringingThe remote party is being alerted; no audio yet
answeredBoth parties are connected and audio is flowing
completedCall ended normally
failedA carrier or platform error prevented connection
no_answerRemote party did not answer within the timeout
busyRemote party returned a busy signal
canceledCaller hung up before the remote answered

completed is the only fully successful terminal state. All others (failed, no_answer, busy, canceled) are terminal failure flavours. Your dashboard's "Failed today" stat counts all four.

Inbound call flow

Inbound call flow
Inbound call flowSequence: caller dials → Sautikit receives → POST to voice_callback_url → server returns VoiceAction → PBX executes verbs → hangup and wallet debit.Caller dials +254700000001Sautikit platform receives callPOST {voice_callback_url}call state + digits (if any)Your server returns VoiceAction responsePBX executes verbsSay · GetDigits · Dial · …call.hangup → cost debited from wallet

The platform signs each webhook POST with X-Sautikit-Signature. See Webhooks for verification details.

Outbound call flow

Outbound call flow
Outbound call flowPOST /v1/calls/originate → platform dials remote party → POSTs to voice_callback_url on each step → call.hangup with wallet debit.POST /v1/calls/originate{ "to": "+254722000001", "from": "+254700000001", "voice_callback_url": "…" }Platform dials the remote partyPOST {voice_callback_url} on each stepcall.hangup → cost debited from wallet

You must own the from number and it must be in active status. The voice_callback_url may differ from the number's default routing, which is useful for per-campaign overrides.

Cost calculation

Cost is calculated only on answered calls:

cost_minor = ceil(duration_seconds / 60) × rate_per_minute_minor

Where rate_per_minute_minor is the per-workspace KES rate at call time. A 90-second call is billed as 2 minutes. A call that never reaches answered costs nothing.

The debit is atomic: the platform writes a wallet_ledger entry and stamps ledger_entry_id on the call record in a single transaction. If the debit fails (insufficient funds), the call is still completed; the balance may briefly go negative by one call's cost. Calls are then blocked until the wallet is topped up.

Call records and events

Every call has:

  • A calls row with the summary (direction, status, cost_minor, duration_seconds, etc.)
  • Zero or more call_events rows with granular telemetry (call.ringing, call.answered, etc.)

Query calls via GET /v1/calls (supports filtering by status, direction, date range, and remote number substring).

Recording lifecycle

Enable recording by including a Record verb in your VoiceAction response, or by setting record on a Dial verb.

Recording lifecycle
Recording lifecycleRecording captured by PBX → stored in object storage → recording.ready event fired → presigned URL returned via API.recording captured by PBXstored in object storageS3-compatiblecall.recording.ready event fired to events_urlGET /v1/calls/{id}/recording→ 302 to 15-min presigned URL

Recordings are stored in the object storage bucket attached to your workspace's storage tier. The API never streams audio through the Go process; it returns a short-lived presigned URL. Default free storage is 1 GB. Recordings older than your retention period are pruned; the API returns 410 calls.recording_expired for those.

Next steps

  • Place your first outbound call
  • Calls API reference
  • Voice actions DSL