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 — a kalba_test_ key first (full flow, zero telephony), your kalba_live_ key after UAT.
export KALBA="https://kalba.ai/api"
export KEY="kalba_test_..."

Contents

  1. What provisioning sets up for you
  2. Use case 1 — inbound customer-service line
  3. Use case 2 — payment-due reminder call
  4. Use case 3 — payment-date change, end to end
  5. Use case 4 — arrears assistance
  6. Use case 5 — event-driven dialing: partner.payment_overdue
  7. Identity verification flow
  8. Servicing outcomes reference
  9. Compliance
  10. Onboarding path

What provisioning sets up for you

A servicing account is configured during provisioning with:

ItemWhat it is
Servicing agentYour 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 windowDefault 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 endpointsYour base URL + signing secret so the agent can verify callers and read/act on account data mid-call — see Partner Data Endpoints.
Escalation targetA 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):

  1. Customer dials your provisioned number → agent answers, discloses it is an AI assistant, and hears the request: "noriu perkelti įmokos datą".
  2. Agent collects identifiers (DOB / contract number).
  3. → your POST /kalba/tools/identify_customer{ "verified": true, "customer_ref": "cus_8f31..." }
  4. → your POST /kalba/tools/get_next_payment → agent confirms the current date with the caller.
  5. → your POST /kalba/tools/request_payment_date_change with { "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 answer accepted.
  6. Agent reads your spoken_result (including the fee), confirms, closes.
  7. You receive call.completed, then call.analyzed with the servicing outcome, and reconcile against the change request you logged in step 5 (join on call_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_person outcome 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:

OutcomeMeaningTerminal?
informedCustomer heard and acknowledged the information (incl. "already paid").yes
promise_to_payCustomer committed to pay; details in the analysis.yes
payment_disputeCustomer disputes the amount/debt — route to your team.yes
wrong_personReached someone who is not the customer; nothing disclosed.yes
callbackCustomer asked to be contacted later / left a callback request.yes — your team follows up; not auto-redialed
no_answerNo pickup.retried per campaign policy
voicemailVoicemail reached; no account data left in the message.retried per campaign policy
failedTechnical failure.retried per campaign policy
escalatedLive-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/dnc to suppress (422 dnc_blocked on 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 consent object — basis ("contract" | "consent" | "legitimate_interest"), plus recorded_at and source — set at lead creation and echoed on the Lead object. Campaigns carry a category ("sales" | "service" | "marketing"). Servicing calls to your own customers about their own account run as service with consent.basis: "contract". Marketing campaigns dial only leads with consent.basis: "consent" — anything else is skipped with per-item status no_consent (batch) or rejected 422 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 with queue_if_closed: true / schedule_at. Inbound is answered 24/7.
  • Your side. Lawful-basis bookkeeping (what you assert in consent.basis must be true in your records), responsible-lending rules on what may be said about arrears, and acting on payment_dispute / callback outcomes 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:

  1. Test keys. You get kalba_test_ keys on day one. The entire API works with zero telephony: test calls advance queued → in_progress → completed in ~1–2 minutes with canned transcripts and deterministic outcomes, so you build webhook consumers, event polling, and state handling against real asynchronous behavior.
  2. 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.
  3. 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.
  4. 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.
  5. 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.