Build calling into your application
Use your backend to control calls, invite people, receive events, and request optional recording analysis.
Base URL: https://api.kallavoice.com
Live carrier access is restricted to approved test accounts. A sandbox key does not activate a SIP connection, enable arbitrary destinations, or incur a subscription charge.
1. Create a scoped API key
Sign in to your verified account, enable the features you need, and open Application API keys. Give the key a name, select its permissions and expiry, and save the secret when it appears. Kalla shows the secret only once. Create a replacement key before revoking an old one when rotating credentials.
Keep the key on your backend. Do not put it in browser JavaScript, mobile bundles, URLs, analytics or logs. General API requests with an Origin header are rejected. Browser audio uses short-lived grants issued by your backend instead.
curl https://api.kallavoice.com/v1/capabilities \
-H "Authorization: Bearer $KALLA_API_KEY"Capabilities require both the key's permission and the account's enabled feature. Revocation or removing a feature also blocks queued operations before dispatch. Keys expire after 1, 7, 30 or 90 days; your account can hold up to ten unexpired active keys.
GET /v1/capabilities returns tenant, keyId, enabled, available and apiVersion. Account sign-in and key management use your session cookie and CSRF protection; an API key cannot manage another account.
2. Create and control a call
Mutations use JSON. Call creation, commands, programs, invitations and analysis jobs require an Idempotency-Key. Reuse that value only when retrying the same request. Changed content returns a conflict. A 202 response means accepted, not answered.
curl https://api.kallavoice.com/v1/calls \
-H "Authorization: Bearer $KALLA_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: example-call-001' \
-d '{"to":"+12025550101","from":"+12025550102","browserSubject":"staff-123"}'The numbers above are reserved examples and work only in an operator-configured fixture tenant. Use only your account's approved destinations and caller IDs. Permissions: calls.create; browser preparation also needs browser.grants. Optional context is {"type":"customer"|"user","id":"your-record-id"}. Your backend authenticates the user and authorizes access to that record.
The receipt contains id, callId and status. Poll GET /v1/commands/{id} until completed. An uncertain result may already have performed the action: do not create another call to retry it.
| Endpoint | Required permission / behavior |
|---|---|
| GET /v1/calls GET /v1/calls/{call} | calls.read. Call state, version, participant mapping and retained context. |
| POST /v1/calls/{call}/commands | calls.control plus operation-specific permission. Body: operation, version, args. |
| GET /v1/commands/{command} | calls.read. Completed, rejected and uncertain outcomes remain distinguishable. |
| POST /v1/calls/{call}/grants | browser.grants. Body: {"subject":"authenticated-user-id"}. Returns one-use token, callId, subject and expiry. |
| GET /v1/routing PUT /v1/routing | routing. PUT: version, queues, aiAdapters, fallback. Current fallback value: voicemail. |
Commands: hangup, hold, resume, connect_destination use empty args. add_participant takes destination; remove_participant takes participantId (both require conferences). queue takes queueId and optional timeoutSeconds; pickup_queue takes destination and subject (queues). voicemail takes empty args (voicemail). The live driver does not yet accept ai_handoff.
{"operation":"hangup","version":3,"args":{}}Read the current call version before a new command. On a version conflict, inspect state before deciding what to do next; never blindly redial.
Browser and native-cell audio
For browserSubject calls, your backend waits for browser.media_ready before issuing connect_destination. No customer or group member rings before the initiating browser is connected. Browser grants are passed to the Kalla audio client at https://media.kallavoice.com/rtc/, never an account key. The authenticated backend determines subject identity; a caller-supplied UID is not authentication.
For staff-cell calling, replace browserSubject with callerCell (cell.bridges permission). Staff must answer before Kalla connects the destination. Both numbers must be allowed by the tenant. Native-cell and browserSubject cannot be combined. Carrier limits remain operator-controlled during beta.
3. Invite someone or claim an incoming call
Create an invitation with POST /v1/calls/{call}/invitations: {"subject":"staff-123","purpose":"answer"}. Purpose is answer (one staff answer owner) or participant (add a person). Required permissions: calls.control, conferences and browser.grants. Invitations expire after 60 seconds unless joined.
GET /v1/invitations?subject=staff-123 lists the subject's invitations. GET /v1/invitations/{id} returns id, call_id, subject, state, version and expires. Both need calls.read.
POST /v1/invitations/{id}/decisions
{"subject":"staff-123","claimId":"device-attempt-unique-id","action":"accept","version":1}Actions: accept, decline, busy, cancel. One device wins; another staff member's answer invite becomes answered_elsewhere. Cancellation removes the invited leg and releases its claim without ending the parent caller. A backend native-cell accept can additionally supply callerCell (cell.bridges required).
For an incoming browser screen, the backend POSTs an empty object to /v1/invitations/{id}/token. Give only that restricted token and invitation ID to https://media.kallavoice.com/rtc/incoming.html. It supports accept/decline/busy for that invitation, not arbitrary calling or account access. Do not embed the token in a URL.
Full saved-group orchestration and transfers remain under development. Native group joins require pressing 1; a carrier answer alone does not qualify. The private prompt waits up to eight seconds after playback. A wrong digit, timeout, revoked invitation, or ended parent prevents joining. This applies to add_participant and participant-purpose cell invitations; answering the original incoming call is separate.
4. Run a bounded call script
POST /v1/programs/validate with {"xml":"..."} checks the XML subset, not runtime eligibility. To run it, POST {"xml":"...","version":3} to /v1/calls/{call}/programs, with an Idempotency-Key. Requires calls.control and routing plus permissions for included operations. Poll GET /v1/programs/{id}.
<Response>
<Say>Thank you for calling.</Say>
<Hangup/>
</Response>Implemented subset: Say, Play, Pause, Hangup, signed POST Redirect, DTMF Gather, bounded Record, simultaneous Number dialing, moderated conferences and terminal Enqueue. Programs must end with Hangup, Redirect or Enqueue. Unsupported behavior fails preflight.
Gather requires an HTTPS action callback on an operator-registered tenant callback origin. Speech recognition, Dial Client, Dial recording, Number URL/status callbacks, conference waitUrl/beeps/recording/status callbacks, and Enqueue action/waitUrl are not supported by this runtime. Do not assume full TwiML compatibility.
GET /v1/recordings and GET /v1/recordings/{id}/audio require recordings.read. WAV download is available only after completion and remains tenant-scoped.
5. Optional recording intelligence
Separate features: transcription, keywords, tone, score and summary. Enable each feature and grant its corresponding intelligence.* permission. These are assessments, not authoritative facts about a person. No model output executes a CRM action.
POST /v1/intelligence/profiles requires intelligence.configure and each requested feature. Exact fields: provider (gemini or self-hosted), model, credential, endpoint, features, language, rubric, maxJobsPerDay (1–100), cloudEgressAllowed and automatic. A score requires a rubric. Provider credentials are encrypted and never returned.
{
"provider":"self-hosted", "model":"your-model",
"credential":"YOUR_PROVIDER_SECRET", "endpoint":"https://your-service.example/analyze",
"features":["transcription","summary"], "language":"en",
"rubric":"", "maxJobsPerDay":10, "cloudEgressAllowed":false, "automatic":false
}Automatic mode queues new completed recordings after activation. No silent cloud fallback. Self-hosted inference must implement Kalla's JSON audio contract; private endpoints require operator registration. Gemini requires explicit cloud egress consent. No model quality or live pricing is guaranteed in beta.
POST /v1/intelligence/jobs with recordingId and profileId; GET or DELETE /v1/intelligence/jobs/{id}. Delete clears derived output while retaining a tombstone. DELETE /v1/intelligence/profiles/{id} disables future processing. Reads require intelligence.read, recordings.read and the profile's feature permissions. Deletion/configuration additionally requires intelligence.configure. In-flight provider requests cannot be recalled. Jobs with uncertain outcomes are never automatically resubmitted.
6. Events and webhooks
GET /v1/events?after=0 requires events.read. Returns events and nextCursor. Persist your cursor. Events include id, sequence, callId, type, data, occurredAt and schemaVersion. Use event IDs to deduplicate; late events must not reopen a finished call.
Webhook registration is operator-managed during beta. Signed JSON delivery is at least once. Headers: X-Phone-Event-Id, X-Phone-Timestamp, X-Phone-Signature. Verify HMAC-SHA256 over timestamp + "." + raw_request_body using your webhook secret, compare in constant time, reject timestamps older/newer than five minutes, and deduplicate event IDs. Acknowledge only after durable receipt. Do not parse and reserialize JSON before verification.
Script callbacks use POST form data with X-Phone-Request-Id and the same timestamp/signature construction. They must return bounded XML. Redirects and private-network destinations are rejected.
Dialing and conferences
Dial accepts up to ten distinct allowlisted Number targets. It rings simultaneously, connects the first answered leg and cancels the others. Paid-leg capacity is reserved for all targets before the first ring; failed or cancelled attempts are not refunded automatically. timeout is 1–30 seconds and timeLimit is 1–300 seconds. answerOnBridge supports true only. Number sendDigits accepts up to 32 keypad digits (0–9, * and #). This is first-answer dialing, not staff group admission.
<Response>
<Dial timeout="20" timeLimit="120" answerOnBridge="true">
<Number>+12025550101</Number>
<Number sendDigits="123#">+12025550103</Number>
</Dial>
<Hangup/>
</Response>Numbers above are examples; your tenant and activated carrier must permit every target and caller ID.
A Conference must be the only Dial child. Room names are scoped to your tenant. Supported attributes: startConferenceOnEnter, endConferenceOnExit, muted, maxParticipants (2–25), participantLabel (unique per room), and beep="false". Members wait in their existing private call until a starter arrives. A member with endConferenceOnExit ends the room on leaving; the other scripts continue. Capacity counts joined call sessions, which may already contain multiple audio legs. Attach browser audio before starting the program. Current worker/call capacity may be lower than maxParticipants. A controller restart ends conference calls rather than reconstructing uncertain membership.
<Response>
<Dial timeLimit="120">
<Conference startConferenceOnEnter="true" endConferenceOnExit="true"
maxParticipants="8" participantLabel="moderator" beep="false">Sales</Conference>
</Dial>
<Hangup/>
</Response>Bring your own carrier
In your account, enroll a TLS/SRTP SIP connection using IP authentication, digest credentials without registration, or outbound registration. Secrets are encrypted and are never returned in account status. Supply your provider's public IPv4 source networks and a SIP hostname with a valid TLS certificate.
- Enter inbound numbers, explicit outbound destinations, and requested call-count/duration limits. Site requests expire after seven days and do not authorize spending.
- Run Check DNS and TLS. Private DNS answers are rejected. A successful certificate check confirms transport reachability only.
- An operator verifies SIP authentication, SRTP media and number ownership, reviews your requested budget, and installs the private configuration bundle with pinned DNS, source ACLs and per-connection routing authentication.
- Activation requires the matching installation receipt and fresh validation. Until then, no carrier route is enabled. Suspend connection stops new inbound and outbound calls; it does not interrupt an existing call.
The provider must support the private X-Kalla-Route header on incoming calls. Shared provider IP ranges alone never select a tenant. Duplicate DID claims are rejected. Activation and budget approval are operator actions, unavailable to customer API keys. No number is ported or rerouted automatically. Existing test usage is preserved when a legacy test trunk is adopted. Providers without TLS/SRTP or the required inbound header are not supported by this release.
Errors and operational limits
Error bodies are {"error":"machine_readable_code"}.
| Status | Meaning |
|---|---|
| 400 | Invalid or unsupported request. Correct it before retrying. |
| 401 / 403 | Invalid key, insufficient feature/scope, wrong origin or disallowed destination. |
| 404 | Resource absent from the authenticated tenant. |
| 409 | State/version conflict, request already claimed, unavailable feature or capacity/budget limit. Inspect before retrying. |
| 429 | Rate or quota limit. Back off; do not create replacement operation IDs. |
General API requests are limited to 16 KiB. Recording analysis accepts bounded completed WAV files, not arbitrary remote audio URLs. Beta enrollment does not activate public carrier routing or live billing. Maintain caller consent and your approved recording policy. Emergency calling is not enabled by this beta.