# AI Calling API - Third Party Integration This document describes the API for third-party CRMs to integrate with the AI calling service. --- ## Overview The AI Calling API allows third-party CRMs to initiate AI-powered outbound calls for lead qualification. The API provides a simple, abstracted interface - you provide lead info and agent configuration, we handle all the underlying voice AI infrastructure. ### Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ YOUR CRM (Third Party) │ │ - Lead data │ │ - Agent config (company, name, voice, language) │ │ - Script (greeting, questions, instructions) │ │ - Webhook URLs │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ SOCIALSELLING AI API │ │ - Authentication & rate limiting │ │ - Input validation │ │ - Voice/language mapping │ │ - Prompt engineering │ │ - Call orchestration │ │ - Webhook delivery │ │ - Billing & credits │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ INTERNAL INFRASTRUCTURE (Hidden from you) │ │ - Voice AI engine │ │ - Speech recognition │ │ - Text-to-speech │ │ - LLM processing │ │ - Telephony providers │ │ - Recording storage │ └─────────────────────────────────────────────────────────────────┘ ``` ### What We Provide - Single call initiation endpoint - Real-time webhooks for call events - Classification results (hot/warm/cold) - Call transcripts and recordings - Per-minute billing - Fully managed voice AI infrastructure ### What You Handle - Bulk/batch orchestration - Scheduling and business hours logic - Retry logic on failures - Queue management - Concurrency control (within our limits) --- ## Authentication All API requests require authentication via Bearer token. ```http Authorization: Bearer Content-Type: application/json ``` API tokens are issued per integration account. Contact support to obtain credentials. --- ## Base URL ``` Production: https://api.socialselling.ai/v1 Sandbox: https://sandbox.socialselling.ai/v1 ``` --- ## Rate Limits | Limit | Value | |-------|-------| | Max concurrent calls | 20 (configurable per account) | | Requests per minute | 100 | | Daily call limit | Based on plan | When limits are exceeded, you'll receive `429 Too Many Requests`. --- ## Endpoints ### 1. Initiate Call Start an AI call to a single lead. ```http POST /ai-call/client/initiate ``` #### Request Body ```json { "lead": { "id": "your-lead-id-123", "name": "John Doe", "phone_number": "+919876543210" }, "agent": { "company_name": "Acme Corp", "agent_name": "Priya", "voice": "female_professional", "language": "en" }, "script": { "greeting": "Hi {lead_name}, this is {agent_name} from {company_name}. Do you have a moment?", "product_context": "We offer enterprise CRM solutions for mid-size businesses.", "questions": [ "Are you currently using any CRM system?", "What is your team size?", "What is your timeline for implementation?" ], "instructions": "Be conversational and friendly. Focus on understanding their pain points." }, "classification": { "hot_threshold": 70, "warm_threshold": 40 }, "webhooks": { "on_classification": "https://your-crm.com/webhooks/ai-classification", "on_call_end": "https://your-crm.com/webhooks/ai-call-complete" }, "metadata": { "campaign_id": "summer-2024", "source": "linkedin-ads" } } ``` #### Request Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `lead.id` | string | Yes | Your unique identifier for the lead | | `lead.name` | string | Yes | Lead's name (used in `{lead_name}` placeholder) | | `lead.phone_number` | string | Yes | Phone number in E.164 format (e.g., `+919876543210`) | | `agent.company_name` | string | Yes | Company name (used in `{company_name}` placeholder) | | `agent.agent_name` | string | Yes | AI agent's name (used in `{agent_name}` placeholder) | | `agent.voice` | string | No | Voice style (default: `female_professional`). See [Voice Options](#voice-options) | | `agent.language` | string | No | Language code (default: `en`). See [Language Codes](#language-codes) | | `script.greeting` | string | No | Custom greeting template with placeholders | | `script.product_context` | string | No | Product/service description for AI context | | `script.questions` | array | No | Qualification questions to ask (max 5) | | `script.instructions` | string | No | Additional behavior instructions for AI | | `classification.hot_threshold` | int | No | Score >= this = hot (default: 70) | | `classification.warm_threshold` | int | No | Score >= this = warm (default: 40) | | `webhooks.on_classification` | string | Yes | URL for classification webhook | | `webhooks.on_call_end` | string | Yes | URL for call completion webhook | | `metadata` | object | No | Custom data (returned unchanged in webhooks) | #### Placeholders Use these placeholders in `greeting` - they are automatically replaced: | Placeholder | Replaced With | |-------------|---------------| | `{lead_name}` | `lead.name` value | | `{agent_name}` | `agent.agent_name` value | | `{company_name}` | `agent.company_name` value | #### Response ```json { "success": true, "call_id": "call_abc123xyz", "message": "Call initiated successfully" } ``` #### Error Response ```json { "success": false, "error": { "code": "INVALID_PHONE", "message": "Phone number format is invalid. Use E.164 format." } } ``` --- ### 2. End Call Terminate an active call. ```http POST /ai-call/client/end ``` #### Request Body ```json { "call_id": "call_abc123xyz" } ``` Or by lead ID: ```json { "lead_id": "your-lead-id-123" } ``` #### Response ```json { "success": true, "message": "Call ended successfully" } ``` --- ### 3. Get Call Status Check status of a specific call. ```http GET /ai-call/client/{call_id}/status ``` #### Response ```json { "call_id": "call_abc123xyz", "lead_id": "your-lead-id-123", "status": "in_progress", "started_at": "2024-01-15T10:30:15Z", "duration_seconds": 45, "classification": null } ``` **Status values:** `queued`, `ringing`, `in_progress`, `completed`, `failed`, `no_answer`, `busy` --- ### 4. Get Account Limits Check your current usage and limits. ```http GET /ai-call/client/account/limits ``` #### Response ```json { "max_concurrent_calls": 20, "active_calls": 5, "available_slots": 15, "daily_limit": 500, "daily_used": 127, "credits_remaining": 1250.5 } ``` --- ### 5. List Available Voices Get available voice options for your account. ```http GET /ai-call/client/voices ``` #### Response ```json { "voices": [ { "id": "female_professional", "name": "Professional Female", "gender": "female", "style": "professional", "sample_url": "https://api.socialselling.ai/samples/female_professional.mp3" }, { "id": "male_friendly", "name": "Friendly Male", "gender": "male", "style": "friendly", "sample_url": "https://api.socialselling.ai/samples/male_friendly.mp3" } ] } ``` --- ### 6. List Supported Languages Get supported languages for your account. ```http GET /ai-call/client/languages ``` #### Response ```json { "languages": [ {"code": "en", "name": "English"}, {"code": "hi", "name": "Hindi"}, {"code": "ml", "name": "Malayalam"}, {"code": "ta", "name": "Tamil"} ] } ``` --- ## Webhooks You must provide webhook URLs when initiating calls. We send POST requests with the following payloads. ### Webhook Security All webhooks include a signature header for verification: ```http X-Signature: sha256= X-Timestamp: 1705312215 ``` Verify using your webhook secret (provided during onboarding): ```python import hmac import hashlib def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool: expected = hmac.new( secret.encode(), f"{timestamp}.{payload.decode()}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature) ``` --- ### Classification Webhook Sent when the AI classifies the lead (during or after the call). ```http POST {your_on_classification_url} ``` ```json { "event": "classification", "call_id": "call_abc123xyz", "lead_id": "your-lead-id-123", "timestamp": "2024-01-15T10:32:45Z", "classification": { "category": "hot", "score": 85, "interest_level": "high", "budget_confirmed": true, "timeline": "1_month", "next_action": "schedule_demo", "summary": "Lead is decision maker, has budget approved for Q2, wants demo next week." }, "metadata": { "campaign_id": "summer-2024", "source": "linkedin-ads" } } ``` #### Classification Fields | Field | Type | Values | |-------|------|--------| | `category` | string | `hot`, `warm`, `cold` | | `score` | int | 0-100 | | `interest_level` | string | `high`, `medium`, `low`, `none` | | `budget_confirmed` | boolean | Whether budget was confirmed | | `timeline` | string | `immediate`, `1_month`, `1_3_months`, `3_plus_months`, `no_timeline` | | `next_action` | string | `schedule_demo`, `send_info`, `schedule_callback`, `add_to_nurture`, `no_action` | | `summary` | string | Brief summary of the conversation | --- ### Call End Webhook Sent when the call ends (success or failure). ```http POST {your_on_call_end_url} ``` ```json { "event": "call_end", "call_id": "call_abc123xyz", "lead_id": "your-lead-id-123", "timestamp": "2024-01-15T10:35:00Z", "status": "completed", "disposition": "connected", "duration_seconds": 185, "started_at": "2024-01-15T10:30:15Z", "ended_at": "2024-01-15T10:33:20Z", "transcript": "Agent: Hi John, this is Priya from Acme Corp...\nLead: Yes, hello...\n...", "recording_url": "https://storage.socialselling.ai/recordings/call_abc123xyz.mp3", "analysis": { "sentiment": "positive", "key_points": [ "Currently using spreadsheets for CRM", "Team of 25 people", "Budget approved for Q2" ], "next_steps": [ "Send product brochure", "Schedule demo for next week" ] }, "classification": { "category": "hot", "score": 85 }, "billing": { "minutes_billed": 4, "credits_consumed": 4.0 }, "metadata": { "campaign_id": "summer-2024", "source": "linkedin-ads" } } ``` #### Disposition Values | Value | Description | |-------|-------------| | `connected` | Call was answered | | `voicemail` | Reached voicemail | | `no_answer` | Phone rang, no answer | | `busy` | Line was busy | | `failed` | Technical failure | | `declined` | Call declined/rejected | --- ## Voice Options Available voices for `agent.voice`: | Voice ID | Name | Gender | Style | |----------|------|--------|-------| | `female_professional` | Professional Female | Female | Clear, professional tone | | `female_friendly` | Friendly Female | Female | Warm, approachable tone | | `female_energetic` | Energetic Female | Female | Upbeat, enthusiastic tone | | `male_professional` | Professional Male | Male | Clear, professional tone | | `male_friendly` | Friendly Male | Male | Warm, approachable tone | | `male_calm` | Calm Male | Male | Relaxed, reassuring tone | All voices support all languages listed below. The AI automatically speaks in the configured language. --- ## Language Codes Supported languages for `agent.language`: | Code | Language | Native Name | |------|----------|-------------| | `en` | English | English | | `hi` | Hindi | हिन्दी | | `ml` | Malayalam | മലയാളം | | `ta` | Tamil | தமிழ் | | `te` | Telugu | తెలుగు | | `kn` | Kannada | ಕನ್ನಡ | | `bn` | Bengali | বাংলা | | `mr` | Marathi | मराठी | | `gu` | Gujarati | ગુજરાતી | | `pa` | Punjabi | ਪੰਜਾਬੀ | | `es` | Spanish | Español | | `fr` | French | Français | | `de` | German | Deutsch | | `pt` | Portuguese | Português | | `ar` | Arabic | العربية | --- ## Error Codes | Code | HTTP Status | Description | |------|-------------|-------------| | `INVALID_PHONE` | 400 | Phone number format is invalid | | `MISSING_REQUIRED_FIELD` | 400 | Required field missing | | `INVALID_VOICE` | 400 | Voice ID not recognized | | `INVALID_LANGUAGE` | 400 | Language code not supported | | `LEAD_NOT_FOUND` | 404 | Lead ID not found | | `CALL_NOT_FOUND` | 404 | Call ID not found | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | | `CONCURRENT_LIMIT_EXCEEDED` | 429 | Max concurrent calls reached | | `INSUFFICIENT_CREDITS` | 402 | Not enough credits | | `WEBHOOK_INVALID` | 400 | Webhook URL is invalid or unreachable | | `INTERNAL_ERROR` | 500 | Internal server error | --- ## SDK & Integration Examples ### Postman Collection Import this collection into Postman for quick testing. ```json { "info": { "name": "SocialSelling AI Calling API", "description": "Third-party AI Calling API for CRM integrations", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ { "key": "baseUrl", "value": "https://api.socialselling.ai/v1" }, { "key": "apiKey", "value": "your_api_key_here" } ], "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{apiKey}}", "type": "string" } ] }, "item": [ { "name": "Initiate Call", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"lead\": {\n \"id\": \"lead-123\",\n \"name\": \"John Doe\",\n \"phone_number\": \"+919876543210\"\n },\n \"agent\": {\n \"company_name\": \"Your Company\",\n \"agent_name\": \"Priya\",\n \"voice\": \"female_professional\",\n \"language\": \"en\"\n },\n \"script\": {\n \"greeting\": \"Hi {lead_name}, this is {agent_name} from {company_name}.\",\n \"questions\": [\n \"Are you currently using any CRM system?\",\n \"What is your team size?\"\n ]\n },\n \"webhooks\": {\n \"on_classification\": \"https://your-crm.com/webhooks/classify\",\n \"on_call_end\": \"https://your-crm.com/webhooks/call-end\"\n }\n}" }, "url": { "raw": "{{baseUrl}}/ai-call/client/initiate", "host": ["{{baseUrl}}"], "path": ["ai-call", "client", "initiate"] } } }, { "name": "End Call", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"call_id\": \"conv_abc123xyz\"\n}" }, "url": { "raw": "{{baseUrl}}/ai-call/client/end", "host": ["{{baseUrl}}"], "path": ["ai-call", "client", "end"] } } }, { "name": "Get Call Status", "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/ai-call/client/:call_id/status", "host": ["{{baseUrl}}"], "path": ["ai-call", "client", ":call_id", "status"], "variable": [ { "key": "call_id", "value": "conv_abc123xyz" } ] } } }, { "name": "Get Account Limits", "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/ai-call/client/account/limits", "host": ["{{baseUrl}}"], "path": ["ai-call", "client", "account", "limits"] } } }, { "name": "List Voices", "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/ai-call/client/voices", "host": ["{{baseUrl}}"], "path": ["ai-call", "client", "voices"] } } }, { "name": "List Languages", "request": { "method": "GET", "url": { "raw": "{{baseUrl}}/ai-call/client/languages", "host": ["{{baseUrl}}"], "path": ["ai-call", "client", "languages"] } } } ] } ``` **To import:** 1. Open Postman 2. Click "Import" → "Raw text" 3. Paste the JSON above 4. Set your `apiKey` variable in the collection --- ### Python SDK Simple Python client for the AI Calling API: ```python import requests from typing import Optional, List, Dict, Any from dataclasses import dataclass @dataclass class CallResult: success: bool call_id: Optional[str] = None error: Optional[str] = None class SocialSellingAIClient: """Python SDK for SocialSelling AI Calling API.""" def __init__(self, api_key: str, base_url: str = "https://api.socialselling.ai/v1"): self.api_key = api_key self.base_url = base_url self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } def initiate_call( self, lead_id: str, lead_name: str, phone_number: str, company_name: str, agent_name: str, webhook_classification: str, webhook_call_end: str, voice: str = "female_professional", language: str = "en", greeting: Optional[str] = None, questions: Optional[List[str]] = None, product_context: Optional[str] = None, instructions: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, ) -> CallResult: """Initiate an AI call to a lead.""" payload = { "lead": { "id": lead_id, "name": lead_name, "phone_number": phone_number }, "agent": { "company_name": company_name, "agent_name": agent_name, "voice": voice, "language": language }, "webhooks": { "on_classification": webhook_classification, "on_call_end": webhook_call_end } } if greeting or questions or product_context or instructions: payload["script"] = {} if greeting: payload["script"]["greeting"] = greeting if questions: payload["script"]["questions"] = questions if product_context: payload["script"]["product_context"] = product_context if instructions: payload["script"]["instructions"] = instructions if metadata: payload["metadata"] = metadata try: response = requests.post( f"{self.base_url}/ai-call/client/initiate", json=payload, headers=self.headers, timeout=30 ) data = response.json() if response.status_code == 200 and data.get("success"): return CallResult(success=True, call_id=data.get("call_id")) else: error = data.get("detail", {}).get("message") or data.get("message") or "Unknown error" return CallResult(success=False, error=error) except Exception as e: return CallResult(success=False, error=str(e)) def end_call(self, call_id: str) -> CallResult: """End an active call.""" try: response = requests.post( f"{self.base_url}/ai-call/client/end", json={"call_id": call_id}, headers=self.headers, timeout=15 ) data = response.json() return CallResult(success=data.get("success", False)) except Exception as e: return CallResult(success=False, error=str(e)) def get_call_status(self, call_id: str) -> Dict[str, Any]: """Get status of a call.""" response = requests.get( f"{self.base_url}/ai-call/client/{call_id}/status", headers=self.headers, timeout=15 ) return response.json() def get_account_limits(self) -> Dict[str, Any]: """Get account limits and usage.""" response = requests.get( f"{self.base_url}/ai-call/client/account/limits", headers=self.headers, timeout=15 ) return response.json() def list_voices(self) -> List[Dict[str, str]]: """List available voices.""" response = requests.get( f"{self.base_url}/ai-call/client/voices", headers=self.headers, timeout=15 ) return response.json().get("voices", []) def list_languages(self) -> List[Dict[str, str]]: """List supported languages.""" response = requests.get( f"{self.base_url}/ai-call/client/languages", headers=self.headers, timeout=15 ) return response.json().get("languages", []) # Usage Example if __name__ == "__main__": client = SocialSellingAIClient(api_key="your_api_key_here") # Initiate a call result = client.initiate_call( lead_id="lead-123", lead_name="John Doe", phone_number="+919876543210", company_name="Acme Corp", agent_name="Priya", webhook_classification="https://your-crm.com/webhooks/classify", webhook_call_end="https://your-crm.com/webhooks/call-end", greeting="Hi {lead_name}, this is {agent_name} from {company_name}.", questions=["Are you using any CRM?", "What is your team size?"] ) if result.success: print(f"Call initiated! ID: {result.call_id}") # Check status status = client.get_call_status(result.call_id) print(f"Status: {status}") else: print(f"Failed: {result.error}") ``` --- ### Node.js SDK Simple Node.js client for the AI Calling API: ```javascript const axios = require('axios'); class SocialSellingAIClient { /** * Node.js SDK for SocialSelling AI Calling API. * @param {string} apiKey - Your API key * @param {string} baseUrl - API base URL (optional) */ constructor(apiKey, baseUrl = 'https://api.socialselling.ai/v1') { this.apiKey = apiKey; this.baseUrl = baseUrl; this.client = axios.create({ baseURL: baseUrl, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 }); } /** * Initiate an AI call to a lead. */ async initiateCall({ leadId, leadName, phoneNumber, companyName, agentName, webhookClassification, webhookCallEnd, voice = 'female_professional', language = 'en', greeting = null, questions = null, productContext = null, instructions = null, metadata = null }) { const payload = { lead: { id: leadId, name: leadName, phone_number: phoneNumber }, agent: { company_name: companyName, agent_name: agentName, voice, language }, webhooks: { on_classification: webhookClassification, on_call_end: webhookCallEnd } }; if (greeting || questions || productContext || instructions) { payload.script = {}; if (greeting) payload.script.greeting = greeting; if (questions) payload.script.questions = questions; if (productContext) payload.script.product_context = productContext; if (instructions) payload.script.instructions = instructions; } if (metadata) { payload.metadata = metadata; } try { const response = await this.client.post('/ai-call/client/initiate', payload); return { success: true, callId: response.data.call_id }; } catch (error) { return { success: false, error: error.response?.data?.detail?.message || error.message }; } } /** * End an active call. */ async endCall(callId) { try { const response = await this.client.post('/ai-call/client/end', { call_id: callId }); return { success: response.data.success }; } catch (error) { return { success: false, error: error.message }; } } /** * Get status of a call. */ async getCallStatus(callId) { const response = await this.client.get(`/ai-call/client/${callId}/status`); return response.data; } /** * Get account limits and usage. */ async getAccountLimits() { const response = await this.client.get('/ai-call/client/account/limits'); return response.data; } /** * List available voices. */ async listVoices() { const response = await this.client.get('/ai-call/client/voices'); return response.data.voices; } /** * List supported languages. */ async listLanguages() { const response = await this.client.get('/ai-call/client/languages'); return response.data.languages; } } // Usage Example async function main() { const client = new SocialSellingAIClient('your_api_key_here'); // Initiate a call const result = await client.initiateCall({ leadId: 'lead-123', leadName: 'John Doe', phoneNumber: '+919876543210', companyName: 'Acme Corp', agentName: 'Priya', webhookClassification: 'https://your-crm.com/webhooks/classify', webhookCallEnd: 'https://your-crm.com/webhooks/call-end', greeting: 'Hi {lead_name}, this is {agent_name} from {company_name}.', questions: ['Are you using any CRM?', 'What is your team size?'] }); if (result.success) { console.log(`Call initiated! ID: ${result.callId}`); // Check status const status = await client.getCallStatus(result.callId); console.log('Status:', status); } else { console.log(`Failed: ${result.error}`); } } main().catch(console.error); module.exports = SocialSellingAIClient; ``` --- ### TypeScript Types For TypeScript users: ```typescript interface Lead { id: string; name: string; phone_number: string; } interface AgentConfig { company_name: string; agent_name: string; voice?: 'female_professional' | 'female_friendly' | 'female_energetic' | 'male_professional' | 'male_friendly' | 'male_calm'; language?: string; } interface ScriptConfig { greeting?: string; product_context?: string; questions?: string[]; instructions?: string; } interface WebhookConfig { on_classification: string; on_call_end: string; } interface InitiateCallRequest { lead: Lead; agent: AgentConfig; script?: ScriptConfig; classification?: { hot_threshold?: number; warm_threshold?: number; }; webhooks: WebhookConfig; metadata?: Record; } interface CallResult { success: boolean; call_id?: string; message?: string; } interface CallStatus { call_id: string; status: 'queued' | 'ringing' | 'in_progress' | 'completed' | 'failed' | 'no_answer' | 'busy'; duration_seconds: number; started_at?: string; } interface ClassificationWebhook { event: 'classification'; call_id: string; lead_id: string; timestamp: string; classification: { category: 'hot' | 'warm' | 'cold'; score: number; interest_level: string; budget_confirmed: boolean; timeline: string; next_action: string; summary: string; }; metadata?: Record; } interface CallEndWebhook { event: 'call_end'; call_id: string; lead_id: string; timestamp: string; status: 'completed' | 'failed' | 'no_answer' | 'busy'; disposition: string; duration_seconds: number; transcript?: string; recording_url?: string; classification?: { category: 'hot' | 'warm' | 'cold'; score: number; }; billing?: { minutes_billed: number; credits_consumed: number; }; metadata?: Record; } ``` --- ## Bulk Calling (Your Implementation) We provide single-call endpoints. You implement bulk/batch logic on your side. This gives you full control over: - Concurrency management - Scheduling (time of day, business hours) - Priority ordering - Retry logic - Pause/resume functionality ### Recommended Pattern ```python import asyncio import aiohttp class AICallOrchestrator: def __init__(self, api_key: str, max_concurrent: int = 15): self.api_key = api_key self.max_concurrent = max_concurrent # Stay under the 20 limit self.semaphore = asyncio.Semaphore(max_concurrent) async def call_lead(self, session: aiohttp.ClientSession, lead: dict, config: dict): async with self.semaphore: response = await session.post( "https://api.socialselling.ai/v1/ai-call/client/initiate", json={ "lead": lead, "agent": config["agent"], "script": config["script"], "classification": config["classification"], "webhooks": config["webhooks"], }, headers={"Authorization": f"Bearer {self.api_key}"} ) return await response.json() async def call_batch(self, leads: list, config: dict): async with aiohttp.ClientSession() as session: tasks = [self.call_lead(session, lead, config) for lead in leads] results = await asyncio.gather(*tasks, return_exceptions=True) return results # Usage orchestrator = AICallOrchestrator(api_key="your_token", max_concurrent=15) leads = [ {"id": "1", "name": "John", "phone_number": "+919876543210"}, {"id": "2", "name": "Jane", "phone_number": "+919876543211"}, # ... more leads ] config = { "agent": { "company_name": "Acme Corp", "agent_name": "Priya", "voice": "female_professional", "language": "en" }, "script": { "questions": ["Are you using CRM?", "Team size?"] }, "classification": {"hot_threshold": 70, "warm_threshold": 40}, "webhooks": { "on_classification": "https://your-crm.com/webhooks/classify", "on_call_end": "https://your-crm.com/webhooks/call-end" } } results = asyncio.run(orchestrator.call_batch(leads, config)) ``` ### Webhook Handler (Your Side) ```python from fastapi import FastAPI, Request, HTTPException import hmac import hashlib app = FastAPI() WEBHOOK_SECRET = "your_webhook_secret" def verify_signature(payload: bytes, signature: str, timestamp: str) -> bool: expected = hmac.new( WEBHOOK_SECRET.encode(), f"{timestamp}.{payload.decode()}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature) @app.post("/webhooks/classify") async def handle_classification(request: Request): body = await request.body() signature = request.headers.get("X-Signature") timestamp = request.headers.get("X-Timestamp") if not verify_signature(body, signature, timestamp): raise HTTPException(status_code=401, detail="Invalid signature") data = await request.json() lead_id = data["lead_id"] classification = data["classification"] # Your business logic: # - Update lead status in your CRM # - Assign to sales rep if hot # - Create follow-up task # - Send notification return {"status": "received"} @app.post("/webhooks/call-end") async def handle_call_end(request: Request): body = await request.body() signature = request.headers.get("X-Signature") timestamp = request.headers.get("X-Timestamp") if not verify_signature(body, signature, timestamp): raise HTTPException(status_code=401, detail="Invalid signature") data = await request.json() # Store transcript, recording URL, update lead status lead_id = data["lead_id"] transcript = data.get("transcript") recording_url = data.get("recording_url") # Trigger next call in your queue if needed return {"status": "received"} ``` --- ## Billing | Item | Cost | |------|------| | Connected call | 1 credit per minute (rounded up) | | No answer / Busy / Failed | 0 credits | **Examples:** - 45-second call = 1 credit - 2 min 15 sec call = 3 credits - No answer = 0 credits Check your balance anytime via `GET /ai-call/client/account/limits`. --- ## Sandbox Testing Use sandbox environment for testing without charges: ``` https://sandbox.socialselling.ai/v1 ``` Sandbox behavior: - Calls connect to simulated leads - AI responses are simulated - No actual phone calls made - Webhooks fire normally with test data - No credits consumed --- ## Example: Complete Flow ### 1. Initiate Call ```bash curl -X POST https://api.socialselling.ai/v1/ai-call/client/initiate \ -H "Authorization: Bearer your_api_token" \ -H "Content-Type: application/json" \ -d '{ "lead": { "id": "lead_123", "name": "Rahul Kumar", "phone_number": "+919876543210" }, "agent": { "company_name": "TechCorp", "agent_name": "Priya", "voice": "female_professional", "language": "hi" }, "script": { "greeting": "नमस्ते {lead_name}, मैं {company_name} से {agent_name} बोल रही हूं।", "questions": [ "क्या आप नया सॉफ्टवेयर खरीदने में रुचि रखते हैं?", "आपका बजट क्या है?" ] }, "webhooks": { "on_classification": "https://your-crm.com/webhooks/classify", "on_call_end": "https://your-crm.com/webhooks/call-end" } }' ``` ### 2. Receive Classification Webhook ```json { "event": "classification", "call_id": "call_abc123", "lead_id": "lead_123", "classification": { "category": "hot", "score": 85, "summary": "Lead interested in software, budget 50k-1L, decision in 2 weeks" } } ``` ### 3. Receive Call End Webhook ```json { "event": "call_end", "call_id": "call_abc123", "lead_id": "lead_123", "status": "completed", "duration_seconds": 120, "transcript": "Agent: नमस्ते Rahul Kumar...\nLead: हां, बोलिए...", "classification": {"category": "hot", "score": 85}, "billing": {"minutes_billed": 2, "credits_consumed": 2.0} } ``` --- ## Support - **Email:** api-support@socialselling.ai - **Documentation:** https://docs.socialselling.ai - **Status Page:** https://status.socialselling.ai --- ## Campaigns (bulk calling) — added 1 Sep 2026 Everything above is per-lead: you orchestrate. Campaigns move the bulk work to us — queueing, the calling window, consent checks, retries and outcome delivery — with the same authentication, the same lead fields and the same `call.ended` webhook. Base path: `/v1/ai-call/client/campaigns` (same host and Bearer token as `/client/initiate`). ### Create a campaign ```http POST /v1/ai-call/client/campaigns ``` ```json { "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": [ { "id": "your-lead-id-123", "name": "Sreeja P", "phone_number": "+919876543210", "language": "ml", "product": "Individual Loan", "branch": "Kakkanad", "district": "Ernakulam", "consent_source": "branch_form", "consented_at": "28/08/2026" } ] } ``` | Field | Notes | |---|---| | `leads[].id` | Your lead id, echoed as `lead.id` / `lead_id` in every event. (`external_id` also accepted.) | | `leads[].phone_number` | E.164 or 10-digit Indian mobile. (`phone` also accepted.) | | `leads[].consent_source`, `consented_at` | Consent evidence. On a promotional campaign a lead without it is **not dialed** and is reported as `gate_blocked`. | | `webhooks.on_call_end` | Per-campaign delivery URL (https). Optional if a default was set with `PUT …/campaigns/webhook`. | | `metadata` | Echoed in every `call.ended` event for this campaign. | | `auto_start` | Start immediately. Otherwise the campaign is created as `draft`; start it with `POST …/{campaign_id}/start`. | | `pipeline_id` | Optional; defaults to your account's enabled calling pipeline. | Response: ```json { "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 } ``` Up to 20,000 leads per request. Duplicates (same last-10 digits) inside one request are dropped and counted. ### Control and status | Method | Path | Purpose | |---|---|---| | `GET` | `/v1/ai-call/client/campaigns/me` | account readiness, limits, default pipeline | | `PUT` | `/v1/ai-call/client/campaigns/webhook` | default `on_call_end` URL (`{"url": "https://…", "secret": "optional"}`) | | `GET` | `/v1/ai-call/client/campaigns` | list with counts | | `GET` | `/v1/ai-call/client/campaigns/{campaign_id}` | `state`, `statuses`, `dispositions`, `retry_backlog` | | `POST` | `/v1/ai-call/client/campaigns/{campaign_id}/start` · `/pause` · `/resume` | control (start/resume refused with 422 if the agent identity is incomplete) | | `GET` | `/v1/ai-call/client/campaigns/{campaign_id}/outcomes` | one record per lead, same fields as the webhook | ### The `call.ended` event for campaign calls Delivered once per **finished** call (a completed conversation, or a lead whose retries are exhausted). Carrier retries (busy, no answer) do not produce an event — the final result does. Signed exactly as before: `X-Signature: sha256=HMAC_SHA256(secret, "{X-Timestamp}.{body}")`. Retried 3× (immediately, +30 s, +2 min) on a non-2xx response. ```json { "event": "call.ended", "call_id": "…", "occurred_at": "2026-09-01T05:12:40+00:00", "lead": { "id": "your-lead-id-123", "name": "Sreeja P", "phone_number": "+919876543210" }, "lead_id": "your-lead-id-123", "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" }, "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" } ``` The first block is the envelope you already parse from single calls; the fields after `metadata` are the campaign extension. A captured field the person could not answer clearly arrives as `null` with a `__unclear` note — never a guessed value. **Dispositions:** `connected` · `no_answer` · `busy` · `switched_off` · `callback_requested` · `not_interested` · `wrong_number` · `not_available` · `do_not_call` · `gate_blocked` · `dnd_rejected` · `originate_error`. Retry gaps: no answer 2 h (×3), busy 30 min (×3), switched off next day (×2), callback at the requested time, everything else never. **Calling rules applied by the platform:** 09:30–18:00 IST Mon–Sat · consent required for promotional campaigns · suppressed numbers never dialed · max 2 attempts per number per day · concurrency per plan · automatic pause if the carrier fails >20% of originates in 5 minutes. ### Bulk send and queue control Send large files in chunks and steer the queue from your side. All under `/v1/ai-call/client/campaigns`, same token. | Method | Path | Purpose | |---|---|---| | `POST` | `/` with header `Idempotency-Key: ` | create once — a replay with the same key returns the existing campaign (`idempotent_replay: true`) | | `POST` | `/{campaign_id}/leads` | **append a chunk** (≤ 20,000) to an existing campaign; phones already in it are skipped (`already_in_campaign`), so a chunk can be safely resent | | `GET` | `/{campaign_id}/leads?status=&disposition=&cursor=&limit=` | every queue row: position, priority, status, disposition, attempts, next attempt; paginate with `next_cursor` | | `PATCH` | `/{campaign_id}` | tune live: `priority` (higher campaigns dial first), `concurrency_cap`, `max_dials_per_minute`, `daily_cap_per_phone`, `retry_policy`, `window {start,end,days}` (may only **narrow** the account window), `webhooks.on_call_end`, `metadata` | | `POST` | `/{campaign_id}/leads/{ref}/cancel` · `/reschedule` `{at}` · `/prioritize` `{priority}` · `/retry` `{reset_attempts}` | one lead — `ref` is your lead id, our `lead_id`, or the phone number | | `POST` | `/{campaign_id}/leads/bulk` | `{"action": "cancel|reschedule|prioritize|retry", "refs": [...], "at"?, "priority"?, "reset_attempts"?}` for up to 5,000 refs | | `POST` | `/{campaign_id}/stop` | stop and drain: no new dials, waiting leads cancelled, live calls finish | | `GET` | `/queue` | account-wide live view: calls in flight vs cap, DID pool, window open/next open, per-campaign eligible / parked / calling / done | | `POST` · `DELETE` | `/dnc` `{"phones": [...]}` | your do-not-call list — never dialed again in any campaign (queued rows cancelled immediately); `DELETE` lifts | Rules that always hold, whatever you set: a live call is never cut; the calling window can be narrowed but not widened; concurrency never exceeds your plan; suppressed numbers are never dialed. ```http 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"} ``` ### Production hosts and prepaid minutes (7 Sep 2026) Production base: `https://api.socialselling.ai/v1/ai-call/client/campaigns` (also served on `https://bfsi.socialselling.ai`). Docs: `https://bfsi.socialselling.ai/docs/`. Your plan is a prepaid pack of AI minutes, billed per **connected** minute in 6-second pulses; ring time and unanswered calls are free. `GET …/campaigns/me` now returns `plan` (sku, status, minutes purchased / consumed / remaining, percent used, validity end, days to expiry, concurrency cap); `GET …/campaigns/usage?days=30` returns the balance with burn rate and projected exhaustion plus per-day calls/minutes. When the pack is exhausted or expired, campaigns pause themselves (a live call is never cut); start/resume answer `422 {"error":"plan_blocked","reason":"plan_exhausted"|"plan_expired"}` until a top-up.