Kalba.ai integration guide — account servicing
How a business that carries customer accounts runs its phone servicing on Kalba.ai: an AI-staffed inbound customer-service line, outbound payment reminders, in-call changes written back to your own systems, and event-driven outreach on overdue accounts — all through the API. It applies wherever a customer holds an account with a balance and a schedule: lending, utilities, telecoms, insurance, property management, subscriptions.
The worked examples follow a consumer lender, because that vertical exercises every constraint at once — identity verification before any disclosure, a lawful basis recorded per call, and regulated calling hours. Swap the domain nouns for your own; the API surface is identical.
This guide is walkthrough-style: every step is a copy-pasteable curl. It
assumes you've read the API reference; the in-call data
contract your team hosts is specified separately in
Partner Data Endpoints.
- Base URL:
https://kalba.ai/api - All examples use
$KEY— akalba_test_key first (full flow, zero telephony), yourkalba_live_key after UAT.
export KALBA="https://kalba.ai/api"
export KEY="kalba_test_..."
Contents
- What provisioning sets up for you
- Use case 1 — inbound customer-service line
- Use case 2 — payment-due reminder call
- Use case 3 — payment-date change, end to end
- Use case 4 — arrears assistance
- Use case 5 — event-driven dialing:
partner.payment_overdue - Identity verification flow
- Servicing outcomes reference
- Compliance
- Onboarding path
What provisioning sets up for you
A servicing account is configured during provisioning with:
| Item | What it is |
|---|---|
| Servicing agent | Your dedicated AI agent with use_case: "servicing" — servicing outcome taxonomy (reference), no sales side-effects, your script and tone. |
| Inbound number(s) | Dedicated Lithuanian number(s) attached to your agent. List them any time with GET /v1/phone-numbers. Where a number is provisioned, it is also your outbound caller ID — customers see and can call back the same number. |
| Calling window | Default outbound window is Mon–Fri 08:00–17:00 Europe/Vilnius; consumer-servicing accounts are typically provisioned wider (e.g. 08:00–21:00 incl. weekends). Hard platform ceiling: 07:00–21:59 local time, never overridable. Inbound answers 24/7. |
| Partner data endpoints | Your base URL + signing secret so the agent can verify callers and read/act on account data mid-call — see Partner Data Endpoints. |
| Escalation target | A transfer number for your human team; the agent hands over live when a conversation needs a person (outcome escalated). |
curl -s $KALBA/v1/phone-numbers -H "Authorization: Bearer $KEY"
# → { "data": [ { "id": "pn_...", "object": "phone_number",
# "phone": "+37052041100", "label": "CS line LT",
# "inbound_enabled": true, "agent_id": "agt_..." } ],
# "has_more": false }
Use case 1 — inbound customer-service line
Customers call your provisioned number; the AI answers immediately, verifies identity, answers account questions from your data, files requests, and escalates to your team when needed. You integrate by consuming events — there is nothing to trigger.
1. Register a webhook for the inbound lifecycle:
curl -s -X POST $KALBA/v1/webhook-endpoints \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://api.yourcompany.lt/kalba-hook",
"types": ["call.completed", "call.analyzed", "call.inbound_missed", "tool.failed"] }'
# → 201 { "id": "we_...", "secret": "whsec_..." } ← store the secret NOW (shown once)
2. A customer calls. The conversation runs; the agent uses your
partner data endpoints for anything
account-specific. When the call ends you receive call.completed, then
call.analyzed once the transcript analysis is ready. Inbound Call objects
carry direction and from:
{
"id": "call_9a1c...", "object": "call",
"mode": "live", "direction": "inbound", "source": "inbound",
"from": "+37061234567", "to": "+37052041100",
"status": "completed", "use_case": "servicing",
"outcome": "informed",
"duration_sec": 184, "answered": true,
"analysis_status": "ready",
"created": "2026-08-03T10:12:00Z", "ended_at": "2026-08-03T10:15:04Z"
}
3. Pull the details into your CRM or servicing system:
curl -s $KALBA/v1/calls/call_9a1c.../transcript -H "Authorization: Bearer $KEY"
curl -s $KALBA/v1/calls/call_9a1c.../analysis -H "Authorization: Bearer $KEY"
curl -s $KALBA/v1/calls/call_9a1c.../recording -H "Authorization: Bearer $KEY"
# recording → short-lived signed URL (404 recording_not_ready until available;
# recordings retained 90 days by default)
Missed inbound calls (caller hangs up before the conversation starts) emit
call.inbound_missed — wire it to your follow-up process so no customer
contact attempt is ever silently dropped:
{ "type": "call.inbound_missed", "mode": "live",
"data": { "call": { "direction": "inbound", "from": "+37061234567", ... } } }
The line answers 24/7. Outside your business hours the agent still serves
self-service requests; for matters needing your staff it files a
request_callback on your endpoint and your team calls back in hours.
Use case 2 — payment-due reminder call
Three days before an installment is due, call the customer, remind them of the
amount and date, and answer follow-up questions. One request per customer,
with the account facts passed as variables the agent's script uses directly
({{first_name}}, {{amount_due}}, …).
Schedule the reminder for a specific time — schedule_at (RFC 3339, at
least 2 minutes ahead, at most 30 days):
curl -s -X POST $KALBA/v1/calls \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: reminder-PB-2024-004411-2026-08-15" \
-d '{
"to": "+37061234567",
"schedule_at": "2026-08-12T10:00:00+03:00",
"variables": {
"first_name": "Jonas",
"amount_due": "58.40",
"due_date": "2026-08-15",
"contract_no": "PB-2024-004411"
},
"metadata": { "loan_id": "PB-2024-004411", "reminder_kind": "3d_before" }
}'
# → 202 { "id": "call_7d20...", "status": "scheduled",
# "scheduled_at": "2026-08-12T07:00:00Z", ... }
The call dials automatically when the time arrives (and the
calling window is open). You get a call.scheduled event now
and the normal call.initiated → call.completed → call.analyzed sequence at
dial time.
Cancel if the customer pays early — allowed while still scheduled
(409 once it has been dialed):
curl -s -X DELETE $KALBA/v1/calls/call_7d20... -H "Authorization: Bearer $KEY"
# → 200 { "id": "call_7d20...", "status": "canceled" } + event call.canceled
Call now, or queue politely. An immediate POST /v1/calls outside your
calling window returns 422 outside_calling_hours with a retry_at hint. If
you'd rather have us hold it until the window opens, set queue_if_closed:
curl -s -X POST $KALBA/v1/calls \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "to": "+37061234567", "queue_if_closed": true,
"variables": { "first_name": "Jonas", "amount_due": "58.40", "due_date": "2026-08-15" } }'
# in-window → 202 status "queued" (dials immediately)
# out-of-window → 202 status "scheduled" (dials at next window open)
Batch alternative — campaign leads with metadata. For nightly batches,
load leads into a servicing campaign; every metadata key is available to the
agent's prompt as {{key}} at dial time, and the campaign machinery handles
pacing, retries and windows:
curl -s -X POST $KALBA/v1/campaigns/cmp_srv.../leads \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: reminders-2026-08-12" \
-d '{ "leads": [
{ "phone": "+37061234567", "name": "Jonas Petraitis",
"consent": { "basis": "contract", "source": "loan agreement PB-2024-004411" },
"metadata": { "amount_due": "58.40", "due_date": "2026-08-15",
"contract_no": "PB-2024-004411" } },
{ "phone": "+37069900002", "name": "Rasa Kazlauskienė",
"consent": { "basis": "contract", "source": "loan agreement PB-2024-004902" },
"metadata": { "amount_due": "31.10", "due_date": "2026-08-16",
"contract_no": "PB-2024-004902" } }
] }'
Variable rules (both paths): string values pass through; numbers/booleans are
stringified; nested objects/arrays are skipped. Max 20 keys / 1 KB total
(422 metadata_too_large above that). Reserved keys you can't set:
lead_name, lead_id, lead_email, campaign_name, and anything starting
system__. On a direct call, variables win over lead metadata on key
collision.
Consume the result. Reminder calls resolve to servicing outcomes —
typically informed (heard and acknowledged) or promise_to_pay;
no_answer/voicemail/failed retry per your campaign's retry policy.
Use case 3 — payment-date change, end to end
The full loop: customer calls in, verifies, asks to move the installment date, your system decides in real time, and the result lands back in your servicing system. This is the flow where the API, your partner endpoints, and webhooks all meet.
Sequence (steps 3–5 are POSTs from us to your servers — the partner-endpoint contract):
- Customer dials your provisioned number → agent answers, discloses it is an AI assistant, and hears the request: "noriu perkelti įmokos datą".
- Agent collects identifiers (DOB / contract number).
- → your
POST /kalba/tools/identify_customer→{ "verified": true, "customer_ref": "cus_8f31..." } - → your
POST /kalba/tools/get_next_payment→ agent confirms the current date with the caller. - → your
POST /kalba/tools/request_payment_date_changewith{ "customer_ref": "cus_8f31...", "requested_date": "2026-08-25" }→{ "result": "accepted", "new_date": "2026-08-25", "fee": { "amount": 2.0, "currency": "EUR" }, "spoken_result": "..." }— your system is the source of truth; the change is already applied on your side when you answeraccepted. - Agent reads your
spoken_result(including the fee), confirms, closes. - You receive
call.completed, thencall.analyzedwith the servicing outcome, and reconcile against the change request you logged in step 5 (join oncall_id).
# Step 7 — on the call.analyzed webhook:
curl -s $KALBA/v1/calls/call_9a1c.../analysis -H "Authorization: Bearer $KEY"
# → { "summary": "Klientas patvirtino tapatybę ir perkėlė įmokos datą į rugpjūčio 25 d. ...",
# "sentiment": "positive", "quality_score": 9, "language": "lt" }
curl -s $KALBA/v1/calls/call_9a1c.../transcript -H "Authorization: Bearer $KEY"
If your endpoint answers needs_human, the agent offers a callback (via your
request_callback tool) or a live transfer to your team — outcome callback
or escalated. If your endpoint is down, the agent uses the safe fallback
line, a tool.failed event fires, and nothing is promised to the customer
that your system didn't confirm.
Use case 4 — arrears assistance
Outbound calls to customers behind on payments — the most sensitive flow you will run. The platform mechanics are the same as use case 2 (campaign + metadata, or direct scheduled calls); what changes is framing and script.
Framing: assistance, not collection. The call exists to inform and to find a workable path — not to pressure. Conservative script guidance that we apply with you during agent configuration:
- Open with identity + AI disclosure, then verify before disclosing — the
arrears status itself is account data; if the person doesn't verify, the
agent says only that it's calling about their agreement and offers a
callback (
wrong_personoutcome if it's not the customer at all). - State facts neutrally (amount, days overdue), never characterize the person.
- No threats, no legal-consequence language, no invented deadlines or fees — the agent can only quote figures your endpoints return.
- Always offer options: pay now, agree a date (
promise_to_pay), move the installment (use case 3), or talk to a human (escalated). - If the customer disputes the debt → outcome
payment_dispute; the agent does not argue — it records the dispute and hands off to your team. - One respectful attempt per number per day; retries only on
no_answer/voicemail/failed, within your provisioned calling window.
curl -s -X POST $KALBA/v1/campaigns/cmp_arrears.../leads \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "leads": [
{ "phone": "+37061234567", "name": "Jonas Petraitis",
"consent": { "basis": "contract", "source": "loan agreement PB-2024-004411" },
"metadata": { "days_overdue": "12", "amount_overdue": "58.40",
"contract_no": "PB-2024-004411" } } ] }'
Servicing these calls under the loan agreement is a service category
campaign with consent.basis: "contract" on the lead — see
Compliance for how the consent model works.
What lands back: outcomes promise_to_pay (with the analysis capturing
the promised date), informed, payment_dispute, wrong_person, callback,
escalated — feed them straight into your collections workflow via
call.analyzed webhooks. promise_to_pay and informed are terminal (no
auto-redial); callback is honored by your team, not auto-redialed.
Use case 5 — event-driven dialing: partner.payment_overdue
Instead of batch-loading leads, let your servicing system emit domain events and have Kalba react: the moment an installment becomes overdue, your backend posts one event and the platform dials the customer with the right variables — subject to every guardrail (window, DNC, caps, dedup, consent).
1. Create an automation rule (once) that maps the event to a dial into
your arrears campaign, lifting fields from the event's data into call
variables:
curl -s -X POST $KALBA/v1/automation-rules \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"trigger_type": "partner.payment_overdue",
"campaign_id": "cmp_arrears...",
"action": "dial",
"variables_template": {
"amount_overdue": "{{data.amount_overdue}}",
"days_overdue": "{{data.days_overdue}}",
"contract_no": "{{data.contract_no}}"
},
"enabled": true
}'
# → 201 { "id": "rule_...", "object": "automation_rule", ... }
Manage rules with GET /v1/automation-rules, PATCH /v1/automation-rules/{id}
(e.g. {"enabled": false} to pause), DELETE /v1/automation-rules/{id}.
2. Emit the event from your backend when your ledger flips an account to
overdue. type must match ^partner\.[a-z0-9_]{1,40}$; target the customer
by lead_id or phone:
curl -s -X POST $KALBA/v1/events \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"type": "partner.payment_overdue",
"phone": "+37061234567",
"data": { "amount_overdue": "58.40", "days_overdue": "3",
"contract_no": "PB-2024-004411" }
}'
# → 201 { "id": "evt_...", "object": "event", "type": "partner.payment_overdue", ... }
The event is appended to your ledger (so it is also
replayable and delivered to *-subscribed webhooks), the rule matches, and a
call is dispatched into the campaign's machinery. Out-of-window events dial at
the next window open; DNC-listed and consent-blocked numbers are skipped, and
duplicate events inside the dedup window don't double-dial.
Use any partner.* names that fit your domain — partner.payment_due,
partner.contract_signed, partner.kyc_expiring — one rule each.
Identity verification flow
The invariant across every use case: nothing account-specific is spoken until your system has verified the caller. The full contract is in Partner Data Endpoints; this is the conversation-level flow:
Call connects (inbound or outbound)
|
v
AI disclosure + recording notice
|
v
Account-specific data is needed
|
v
Agent collects identifiers <-------------+
(date of birth / last 4 of |
personal code / contract number) |
| |
v |
POST identify_customer |
(caller's phone auto-attached) |
| |
+----------+----------+ |
| | |
verified: true verified: false |
| | |
| +----------------+
| | attempts left
| |
| v no attempts left
| Nothing account-specific is
| disclosed. Offer request_callback,
| or transfer to a human.
| |
v v
customer_ref returned Outcome: callback /
— account tools escalated / wrong_person
unlocked:
get_account_summary,
get_next_payment,
request_payment_date_change
|
v
Agent answers or acts, reading
your spoken_* strings
|
v
Outcome: informed / promise_to_pay /
payment_dispute
The agent allows a bounded number of verification attempts (misheard digits are normal on the phone) and then stops — it never "helps" a failing caller by hinting at what the right answer would be.
Servicing outcomes reference
Your agent runs with use_case: "servicing", so every analyzed call resolves
to one of:
| Outcome | Meaning | Terminal? |
|---|---|---|
informed | Customer heard and acknowledged the information (incl. "already paid"). | yes |
promise_to_pay | Customer committed to pay; details in the analysis. | yes |
payment_dispute | Customer disputes the amount/debt — route to your team. | yes |
wrong_person | Reached someone who is not the customer; nothing disclosed. | yes |
callback | Customer asked to be contacted later / left a callback request. | yes — your team follows up; not auto-redialed |
no_answer | No pickup. | retried per campaign policy |
voicemail | Voicemail reached; no account data left in the message. | retried per campaign policy |
failed | Technical failure. | retried per campaign policy |
escalated | Live-transferred to your human team. | yes |
The Call object carries "use_case": "servicing" so a mixed integration
(sales + servicing agents on one account) can route on it. Sales agents keep
the sales taxonomy documented in the API reference.
Compliance
What the platform enforces for you, and what remains your side of the line:
- Proactive AI disclosure. Every call — inbound and outbound — opens with the agent identifying itself as an AI assistant calling on your behalf, before any substance. This is non-configurable.
- Recording notice. Calls are recorded for quality and dispute
resolution; the agent states this at call start. Recordings are retrievable
via
GET /v1/calls/{id}/recording(short-lived signed URL) and retained 90 days by default; your provisioning can set a different retention to match your regulatory schedule. - DNC honored at dial time. The do-not-call list is checked when the call
is placed, not only when a lead is created — a number suppressed between
scheduling and dialing is not called.
POST /v1/dncto suppress (422 dnc_blockedon any attempt to dial it);DELETE /v1/dnc/{phone}removes a suppression and is audited — only do this holding a fresh, documented re-consent record on your side. - Consent basis: service vs marketing. Every lead can carry its lawful
basis in a
consentobject —basis("contract"|"consent"|"legitimate_interest"), plusrecorded_atandsource— set at lead creation and echoed on the Lead object. Campaigns carry acategory("sales"|"service"|"marketing"). Servicing calls to your own customers about their own account run asservicewithconsent.basis: "contract". Marketing campaigns dial only leads withconsent.basis: "consent"— anything else is skipped with per-item statusno_consent(batch) or rejected422 consent_required(direct call referencing the lead). The platform will not let a marketing campaign reach a customer who only ever consented to being serviced. - Calling hours. Outbound defaults to Mon–Fri 08:00–17:00 Europe/Vilnius;
your provisioned window (e.g. 08:00–21:00 incl. weekends for consumer
servicing) replaces it account-wide. The platform ceiling — 07:00–21:59
local time — cannot be overridden by any configuration. Out-of-window
requests get
422 outside_calling_hours+retry_at, or queue themselves withqueue_if_closed: true/schedule_at. Inbound is answered 24/7. - Your side. Lawful-basis bookkeeping (what you assert in
consent.basismust be true in your records), responsible-lending rules on what may be said about arrears, and acting onpayment_dispute/callbackoutcomes within your regulatory deadlines.
Onboarding path
The same five steps for every servicing account — each gate is cheap and catches a class of problems before the next:
- Test keys. You get
kalba_test_keys on day one. The entire API works with zero telephony: test calls advancequeued → in_progress → completedin ~1–2 minutes with canned transcripts and deterministic outcomes, so you build webhook consumers, event polling, and state handling against real asynchronous behavior. - Sandbox + partner-endpoint mock. Stand up the ~40-line mock of your data endpoints (or your staging system) on a public HTTPS URL; we point your sandbox agent at it. Now the full conversational loop — identify → look up → act — runs end to end with no real customers and no real money.
- UAT. Your team calls the sandbox agent, works through the script matrix (happy path, failed verification, date-change rejection, endpoint down, dispute, transfer), and signs off transcripts, outcomes, and your webhook-driven reconciliation. Signature verification on your endpoints must be live before sign-off.
- Number provisioning. We provision your dedicated Lithuanian number(s), attach them to your production agent, and set your account calling window. The number is verified with live test calls in both directions.
- Go-live. Switch your integration to the
kalba_live_key, start with a low-volume slice (one campaign, capped dials/day), review the first days' transcripts and outcome distribution together, then ramp.
Your Kalba.ai contact drives steps 4–5 with you; steps 1–3 are self-serve the moment you have keys.