Bheemverse · BFSI Voice Platform · integration guide

BFSI Calling Client API

The bulk counterpart of the single-call API you already integrate: same token, same lead fields, same call.ended webhook — with queueing, the calling window, consent checks and retries handled by the platform.

Production base https://api.socialselling.ai/v1/ai-call/client/campaigns (also https://muthoot.socialselling.ai) Version v1 · 1 Sep 2026 Format JSON over HTTPS

Overview

1 · Push leadsPOST / with your lead recordscampaign created as draft
2 · Review the reportqueued · invalid · duplicates · consentreturned in the same response
3 · StartPOST /{id}/start (or auto_start)dials inside the calling window
4 · Receive outcomessigned webhook per finished callor GET /{id}/outcomes

Every dial passes a compliance gate (calling window, suppression list, consent evidence) before it is placed. Retries follow a disposition-based policy — you never re-push a lead that was busy or did not answer.

Authentication

Send your API key as a bearer token on every request. Keys are issued per account by Bheemverse and delivered out of band; keep them server-side.

Authorization: Bearer ss_bfsi_…
Content-Type: application/json

Check the key and the account state with GET /me:

{
  "workspace": "Muthoot Microfin",
  "default_pipeline_id": "ef56fbbb-…",
  "webhook_configured": false,
  "ready_to_dial": true,
  "readiness_failures": [],
  "limits": {"max_concurrent_calls": 10, "daily_call_limit": 2000}
}

Lead record

FieldRequiredNotes
phone_numberyesE.164 or 10-digit Indian mobile, normalised to +91… (phone also accepted). Invalid numbers are counted, not dialed.
idrecommendedYour CRM's lead id — the same lead.id as in /client/initiate; echoed as lead.id / lead_id in every event (external_id also accepted).
nameUsed by the agent to confirm the person.
languagePreferred language hint (ml, ta, hi, en…). The agent still follows the caller's spoken language.
product, branch, districtStored on the lead; returned in exports.
consent_sourcefor promotional callsWhere consent was obtained (e.g. crm_optin, branch_form). Recording it marks the lead as consented; without it a promotional campaign skips the lead at dial time (gate_blocked).
consented_atDate or datetime; DD/MM/YYYY accepted. Defaults to now.

Duplicates within one push (same last-10 digits) are dropped and counted. A phone already known to the account reuses the existing lead.

Endpoints

Method · pathPurposeBody / response
GET /meAccount, readiness, limitssee above
PUT /webhookSet the hand-back URL (https only){"url": "https://…", "secret": "optional new secret"}
POST /Create a campaign from leads{"name", "auto_start"?, "webhooks": {"on_call_end"}?, "metadata"?, "pipeline_id"?, "leads": [...]}campaign_id, state, report, brand_readiness, without_consent. Max 20,000 leads per call.
GET /List campaigns with countscampaigns[] with counts {total, pending, calling, completed, failed, skipped}
GET /{id}Live statusstate, statuses, dispositions, retry_backlog {parked, next_eligible}
POST /{id}/start · /pause · /resumeControlpause accepts {"reason"}; start/resume are refused (422) if the account's agent identity is incomplete
GET /{id}/outcomesPull all resultsone record per lead — the same shape as the webhook payload

Example — push and start

curl -X POST https://socialselling.ai/v1/ai-call/client/campaigns \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "IL leads — Ernakulam — 1 Sep",
    "auto_start": true,
    "webhooks": {"on_call_end": "https://your-crm.com/webhooks/ai-call-complete"},
    "metadata": {"campaign_id": "IL-2026-09-A"},
    "leads": [
      {"phone_number": "+919876543210", "id": "CRM-88121", "name": "Sreeja P",
       "language": "ml", "product": "Individual Loan", "branch": "Kakkanad",
       "district": "Ernakulam", "consent_source": "branch_form", "consented_at": "28/08/2026"}
    ]
  }'
{
  "campaign_id": "63b3f83e-…", "name": "IL leads — Ernakulam — 1 Sep", "state": "running",
  "report": {"total_rows": 1, "queued": 1, "invalid_phone": 0, "duplicates_in_file": 0,
             "leads_created": 1, "existing_leads_reused": 0, "consent_recorded": 1, "suppressed": 0},
  "brand_readiness": {"ok": true, "failures": []},
  "without_consent": 0
}

Bulk send & queue control

Send big files in chunks and steer the queue from your CRM. Three rules always hold, whatever you set: a live call is never cut, the calling window can be narrowed but never widened, and concurrency never exceeds your plan.

Method · pathPurpose
POST / + Idempotency-Key headerCreate once; a replay with the same key returns the existing campaign.
POST /{id}/leadsAppend a chunk (≤ 20,000). Phones already in the campaign are skipped (already_in_campaign) — a chunk can be safely resent after a network error.
GET /{id}/leads?status=&disposition=&cursor=Every queue row: position, priority, status, disposition, attempts, next attempt. Paginate with next_cursor.
PATCH /{id}Tune live: priority (higher campaigns dial first), concurrency_cap, max_dials_per_minute, daily_cap_per_phone, retry_policy per disposition, window {start, end, days} (narrow only), webhooks.on_call_end, metadata.
POST /{id}/leads/{ref}/cancel · reschedule · prioritize · retryOne lead; ref = your lead id, our lead_id, or the phone.
POST /{id}/leads/bulk{"action", "refs": [...], "at"?, "priority"?, "reset_attempts"?} — up to 5,000 refs.
POST /{id}/stopStop and drain: no new dials, waiting leads cancelled, live calls finish.
GET /queueAccount-wide live view: calls in flight vs cap, DID pool, window open / next open, per-campaign eligible · parked · calling · done.
POST · DELETE /dncYour do-not-call list — never dialed again in any campaign; queued rows cancelled at once.
PATCH /v1/ai-call/client/campaigns/63b3f83e-…
{"priority": 20, "max_dials_per_minute": 30, "window": {"start": "10:00", "end": "17:00"},
 "retry_policy": {"no_answer": {"gap_min": 180, "max_attempts": 2}}}

POST /v1/ai-call/client/campaigns/63b3f83e-…/leads/bulk
{"action": "reschedule", "refs": ["CRM-88121", "CRM-88122"], "at": "2026-09-02T11:00:00+05:30"}

Minutes & usage

Your plan is a prepaid pack of AI minutes. Billing is per connected minute in 6-second pulses; ring time and unanswered calls are free. When the pack is exhausted or its validity ends, campaigns pause themselves (a live call is never cut) and resume after a top-up.

Method · pathPurpose
GET /meNow includes plan: sku, status, minutes_purchased, minutes_consumed, minutes_remaining, percent_used, validity_end, days_to_expiry, concurrency_cap. ready_to_dial is false when the plan is not active.
GET /usage?days=30Full balance with burn rate and projected exhaustion, plus per-day calls / minutes.
Start / resumeRefused with 422 {"error": "plan_blocked", "reason": "plan_exhausted" | "plan_expired"} when the pack cannot fund more calls.
{
  "plan": {"sku": "pilot_100k", "status": "active", "minutes_purchased": 100000,
           "minutes_consumed": 8.9, "minutes_remaining": 99991.1, "percent_used": 0.0,
           "validity_end": "2026-11-06T14:49:28+00:00", "days_to_expiry": 60.0,
           "burn_per_day_7d": 1.3, "projected_exhaustion": null},
  "daily": [{"day": "2026-09-07", "calls": 3, "minutes": 8.9}]
}

Hand-back webhook

One POST per finished call — a completed conversation, or a lead whose retries are exhausted. Retried attempts (busy, no answer) do not produce a webhook; the final result does. Delivery goes to the campaign's webhooks.on_call_end, else the account default set with PUT /webhook; retried three times (immediately, +30 s, +2 min) on any non-2xx response. The event name and envelope are identical to single calls — an existing call.ended handler keeps working; campaign fields are additive.

POST {webhooks.on_call_end}
Content-Type: application/json
X-Timestamp: 1756700000
X-Signature: sha256=<hex HMAC-SHA256(secret, "{X-Timestamp}.{raw body}")>

{
  "event": "call.ended",                         ← the envelope you already parse
  "call_id": "…", "occurred_at": "2026-09-01T05:12:40+00:00",
  "lead": {"id": "CRM-88121", "name": "Sreeja P", "phone_number": "+919876543210"},
  "lead_id": "CRM-88121", "engine": "premium", "duration_s": 214,
  "classification": {"intent": "hot", "score": 82},
  "transcript_summary": "…", "transcript": "…", "recording_url": "https://…",
  "metadata": {"campaign_id": "IL-2026-09-A"},

  "campaign": {"id": "63b3f83e-…", "name": "IL leads — Ernakulam — 1 Sep"},   ← campaign extension
  "status": "completed", "disposition": "connected", "attempts": 1,
  "captures": {
    "person_confirmed": "yes", "interested": "yes", "income_source": "tailoring business",
    "monthly_income": 18000, "marital_status": "married", "spouse_monthly_income": 12000,
    "other_earning_members": 1, "household_incomes": [9000]
  },
  "household_income_total": 39000,
  "sent_at": "2026-09-01T05:12:52+00:00"
}

Verifying the signature

# Python
import hmac, hashlib
expected = hmac.new(SECRET.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256).hexdigest()
ok = hmac.compare_digest(signature_header, f"sha256={expected}")

// Node
const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(`sha256=${expected}`));

Use the raw request body, not a re-serialised object. Respond 2xx quickly and process asynchronously; deliveries are idempotent per lead_id + campaign_id — store the latest.

Dispositions & captures

DispositionMeaningRetried?
connectedconversation took place; see captures
no_answer · busy · switched_offcarrier outcomeyes — 2 h / 30 min / next day, up to 3 attempts
callback_requestedthe person named a timeat that time
not_interestedafter the one permitted persuasion loop; objection in capturesno
wrong_number · not_availableidentity step outcomewrong number: no · not available: later
do_not_callasked never to be called — suppressed immediatelynever
gate_blocked · dnd_rejectedskipped before dialing (no consent / suppression / carrier DND)never
originate_errorcarrier could not place the call1 / 5 / 15 min

Captures (Individual Loan flow v3): person_confirmed, interested, income_source, monthly_income, marital_status, spouse_monthly_income (married only), other_earning_members, household_incomes[] (when > 0), plus household_income_total computed. A field the person could not answer clearly arrives as null with a <field>__unclear note — never a guessed value.

Calling rules

  • Window: 09:30–18:00 IST, Monday–Saturday. Leads pushed outside the window queue and dial when it opens.
  • Consent: promotional campaigns dial only leads with consent evidence; others are reported as gate_blocked. Suppressed numbers (do-not-call, STOP) are never dialed.
  • Frequency: at most 2 attempts per number per day and 5 per lead per campaign.
  • Concurrency: per the account plan; the queue drains inside it automatically.
  • Circuit breaker: if the carrier fails more than 20% of originates in 5 minutes the campaign pauses itself (paused_reason explains); resume when clear.

Errors

StatusWhenBody
401missing/invalid/inactive key{"detail": "Invalid or inactive API key"}
403module not enabled for the account{"detail": {"error": "bfsi_not_enabled"}}
404campaign not in this account
422validation; start refused{"detail": {"error": "brand_validation_failed", "failures": [...]}} or a message
413> 20,000 leads in one pushsplit the file

Go-live checklist

  1. GET /me returns ready_to_dial: true.
  2. PUT /webhook with your https endpoint; verify a signature with the shared secret.
  3. Push a 5-lead test campaign with your own team's numbers, auto_start: false; check the report.
  4. Start it inside the window; watch GET /{id} drain; confirm the webhooks arrive and match by external_id.
  5. Then the first real file.