# 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 <your_api_token>
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=<hmac_signature>
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<string, any>;
}

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<string, any>;
}

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<string, any>;
}
```

---

## 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 `<field>__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: <your id>` | 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://muthoot.socialselling.ai`). Docs: `https://muthoot.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.
