Heppu AI
Calls API

Outbound Call

Initiate an immediate outbound call via LiveKit with real-time room creation

Outbound Call

Initiate an immediate outbound call using LiveKit infrastructure. This endpoint queues the call placement through a CPS-throttled Dial queue — the dial itself happens asynchronously (normally sub-second; paced to the SIP trunk's calls-per-second budget under load). Poll Get Call Details for dial progress and LiveKit room information.

Endpoint

POST /api/v1/calls/outbound

Authentication

Supports both API key and session-based authentication:

# API Key
x-api-key: YOUR_API_KEY

# Bearer Token
Authorization: Bearer YOUR_API_KEY

# Session (for web UI)
Cookie: session=...

Request Body

All fields are validated using Zod schema for type safety.

Required Fields

FieldTypeDescription
agentIdstring (UUID)ID of the voice agent to use
phoneNumberstringContact phone number in E.164 format

Optional Fields

FieldTypeDefaultDescription
callerIdstringAgent's first phoneSpecific phone number to display as caller ID
metadataobjectCustom metadata for the call
priorityinteger (0-10)5Call priority level

Validation

Phone Number Validation (Zod)

Phone number must match E.164 format:

  • Pattern: ^\+[1-9]\d{1,14}$
  • Error message: "Phone number must be in E.164 format (+1234567890)"

Agent ID Validation (Zod)

  • Must be a valid UUID
  • Error message: "Invalid agent ID format"

Priority Validation (Zod)

  • Must be integer between 0 and 10
  • Default: 5

Response

Returns the queued call's details. LiveKit room and SIP identifiers are not part of this response — the dial happens asynchronously, and the Dial queue worker writes them onto the call log as they become available. Poll Get Call Details (GET /api/v1/calls/{id}) to read them.

Response Schema

{
  "data": {
    "id": "string",
    "status": "queued",
    "agentId": "string",
    "phoneNumber": "string",
    "callerId": "string",
    "startedAt": "string",
    "agent": {
      "id": "string",
      "name": "string",
      "type": "voice"
    },
    "metadata": {
      "caller_id": "string",
      "from_phone_number": "string",
      "priority": "integer"
    }
  },
  "message": "Outbound call queued. Poll GET /api/v1/calls/{id} for dial progress."
}

Field Descriptions

  • id: Unique call log identifier
  • status: Always "queued" for newly accepted calls — the dial happens asynchronously
  • agentId: Agent UUID
  • phoneNumber: Contact's phone number
  • callerId: Displayed caller ID
  • startedAt: Call initiation timestamp (ISO 8601)
  • agent: Agent information object
  • metadata: Call metadata including source and priority

The LiveKit identifiers (roomId, jobId for the dispatch, sipParticipantId) are no longer in this response. They appear on GET /api/v1/calls/{id} once the Dial queue places the call. Status progresses queuedconnecting (INVITE sent / ringing) → active/completed, or failed (with failure_reason) / cancelled.

Examples

Basic Outbound Call

curl -X POST https://v2.heppu.ai/api/v1/calls/outbound \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+14155552671"
  }'
const response = await fetch('https://v2.heppu.ai/api/v1/calls/outbound', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    agentId: '550e8400-e29b-41d4-a716-446655440000',
    phoneNumber: '+14155552671'
  })
});

const data = await response.json();
console.log('Call queued:', data.data.id);
console.log('Status:', data.data.status); // "queued"

// Poll for dial progress — room/SIP ids appear once the call is placed
const statusResponse = await fetch(
  `https://v2.heppu.ai/api/v1/calls/${data.data.id}`,
  { headers: { 'x-api-key': 'YOUR_API_KEY' } }
);
const { data: call } = await statusResponse.json();
console.log('Status:', call.status); // queued → connecting → active
console.log('Room ID:', call.roomId);
import requests

response = requests.post(
    'https://v2.heppu.ai/api/v1/calls/outbound',
    headers={'x-api-key': 'YOUR_API_KEY'},
    json={
        'agentId': '550e8400-e29b-41d4-a716-446655440000',
        'phoneNumber': '+14155552671'
    }
)

data = response.json()
print(f"Call queued: {data['data']['id']}")
print(f"Status: {data['data']['status']}")  # "queued"

# Poll for dial progress — room/SIP ids appear once the call is placed
status_response = requests.get(
    f"https://v2.heppu.ai/api/v1/calls/{data['data']['id']}",
    headers={'x-api-key': 'YOUR_API_KEY'}
)
call = status_response.json()['data']
print(f"Status: {call['status']}")  # queued -> connecting -> active
print(f"Room ID: {call['roomId']}")

Call with Custom Caller ID and Metadata

curl -X POST https://v2.heppu.ai/api/v1/calls/outbound \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+14155552671",
    "callerId": "+14155551234",
    "priority": 0,
    "metadata": {
      "customerName": "John Doe",
      "accountId": "ACC-12345",
      "callReason": "urgent_support"
    }
  }'
const response = await fetch('https://v2.heppu.ai/api/v1/calls/outbound', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    agentId: '550e8400-e29b-41d4-a716-446655440000',
    phoneNumber: '+14155552671',
    callerId: '+14155551234',
    priority: 0,
    metadata: {
      customerName: 'John Doe',
      accountId: 'ACC-12345',
      callReason: 'urgent_support'
    }
  })
});

const data = await response.json();
console.log('Urgent call initiated:', data.data);
response = requests.post(
    'https://v2.heppu.ai/api/v1/calls/outbound',
    headers={'x-api-key': 'YOUR_API_KEY'},
    json={
        'agentId': '550e8400-e29b-41d4-a716-446655440000',
        'phoneNumber': '+14155552671',
        'callerId': '+14155551234',
        'priority': 0,
        'metadata': {
            'customerName': 'John Doe',
            'accountId': 'ACC-12345',
            'callReason': 'urgent_support'
        }
    }
)

data = response.json()
print(f"Urgent call initiated: {data['data']}")

Call with Error Handling

curl -X POST https://v2.heppu.ai/api/v1/calls/outbound \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+14155552671"
  }' \
  -w "\nHTTP Status: %{http_code}\n" \
  -s
async function initiateOutboundCall(agentId, phoneNumber, options = {}) {
  try {
    const response = await fetch('https://v2.heppu.ai/api/v1/calls/outbound', {
      method: 'POST',
      headers: {
        'x-api-key': 'YOUR_API_KEY',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        agentId,
        phoneNumber,
        ...options
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error.message);
    }

    const data = await response.json();
    console.log('✓ Call queued successfully');
    console.log(`  Call ID: ${data.data.id}`);
    console.log(`  Status: ${data.data.status}`); // "queued" — poll GET /api/v1/calls/{id} for dial progress

    return data.data;

  } catch (error) {
    console.error('✗ Failed to initiate call:', error.message);
    throw error;
  }
}

// Usage
const call = await initiateOutboundCall(
  '550e8400-e29b-41d4-a716-446655440000',
  '+14155552671',
  { priority: 0 }
);
def initiate_outbound_call(agent_id, phone_number, **kwargs):
    try:
        response = requests.post(
            'https://v2.heppu.ai/api/v1/calls/outbound',
            headers={'x-api-key': 'YOUR_API_KEY'},
            json={
                'agentId': agent_id,
                'phoneNumber': phone_number,
                **kwargs
            },
            timeout=10
        )

        response.raise_for_status()
        data = response.json()

        print('✓ Call queued successfully')
        print(f"  Call ID: {data['data']['id']}")
        print(f"  Status: {data['data']['status']}")  # "queued" — poll GET /api/v1/calls/{id} for dial progress

        return data['data']

    except requests.exceptions.HTTPError as e:
        error_data = e.response.json()
        print(f"✗ Failed to initiate call: {error_data['error']['message']}")
        raise

# Usage
call = initiate_outbound_call(
    '550e8400-e29b-41d4-a716-446655440000',
    '+14155552671',
    priority=0
)

Response Examples

Success Response (201)

{
  "data": {
    "id": "call_xyz789abc",
    "status": "queued",
    "agentId": "550e8400-e29b-41d4-a716-446655440000",
    "phoneNumber": "+14155552671",
    "callerId": "+14155551234",
    "startedAt": "2025-12-01T15:30:00.000Z",
    "agent": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Customer Support Agent",
      "type": "voice"
    },
    "metadata": {
      "caller_id": "+14155551234",
      "from_phone_number": "+14155551234",
      "priority": 5,
      "customerName": "John Doe",
      "accountId": "ACC-12345"
    }
  },
  "message": "Outbound call queued. Poll GET /api/v1/calls/{id} for dial progress."
}

Error Responses

Validation Error (Zod)

{
  "error": {
    "message": "Validation error: Phone number must be in E.164 format (+1234567890)",
    "code": 400
  }
}

Invalid Agent ID Format

{
  "error": {
    "message": "Validation error: Invalid agent ID format",
    "code": 400
  }
}

Agent Not Found

{
  "error": {
    "message": "Agent not found or access denied",
    "code": 404
  }
}

Agent Not Voice Type

{
  "error": {
    "message": "Agent must be a voice agent for outbound calls",
    "code": 400
  }
}

Agent Not Active

{
  "error": {
    "message": "Agent must be active to make calls",
    "code": 400
  }
}

LiveKit Not Configured

{
  "error": {
    "message": "LiveKit service is not configured. Please contact support.",
    "code": 500
  }
}

Call Queueing Failed

{
  "error": {
    "message": "Failed to queue call: Connection timeout",
    "code": 500
  }
}

LiveKit Integration

Call Flow

  1. Validation - Request is validated using Zod schema
  2. Agent Verification - Agent exists, is voice type, and is active
  3. Call Log Creation - Database record created with "initiated" status
  4. Queueing - Call log set to "queued" and a dial event emitted to the Dial queue
  5. Response - Call ID and "queued" status returned to client (201)
  6. Dial Queue Worker - Worker creates the LiveKit room and places the SIP call, throttled to the trunk's calls-per-second (CPS) budget — normally sub-second
  7. Status Update - Call log updated to "connecting" with LiveKit details as the INVITE is sent / phone rings
  8. Call Progress - Status moves to "active" then "completed", or "failed" (with failure_reason) / "cancelled"

Why the queue? Bursts above the SIP trunk's CPS limit used to be rejected outright by the carrier; the Dial queue paces them instead.

LiveKit Details

The Dial queue worker writes LiveKit-specific information onto the call log as it becomes available — read it via Get Call Details (GET /api/v1/calls/{id}):

  • roomId: LiveKit room identifier for this call
  • jobId: LiveKit dispatch/job ID for tracking
  • sipParticipantId: SIP participant identifier

Use these IDs to:

  • Monitor call status via LiveKit API
  • Join the room programmatically
  • Track call metrics and events

Use Cases

Click-to-Call from CRM

async function clickToCall(contactId, agentId) {
  // Fetch contact from CRM
  const contact = await fetchContact(contactId);

  // Initiate immediate call
  const response = await fetch('https://v2.heppu.ai/api/v1/calls/outbound', {
    method: 'POST',
    headers: {
      'x-api-key': 'YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      agentId: agentId,
      phoneNumber: contact.phone,
      priority: 0, // High priority for manual calls
      metadata: {
        contactId: contact.id,
        contactName: contact.name,
        accountId: contact.accountId,
        source: 'crm_click_to_call',
        initiatedBy: 'sales_rep'
      }
    })
  });

  const data = await response.json();

  // Update CRM with call details
  // (room id arrives via GET /api/v1/calls/{id} once the Dial queue places the call)
  await updateCRMCall(contactId, {
    callId: data.data.id,
    status: data.data.status // "queued"
  });

  return data.data;
}

Emergency Call System

def initiate_emergency_call(contact_phone, alert_type, location):
    """Initiate high-priority emergency call"""

    response = requests.post(
        'https://v2.heppu.ai/api/v1/calls/outbound',
        headers={'x-api-key': 'YOUR_API_KEY'},
        json={
            'agentId': EMERGENCY_AGENT_ID,
            'phoneNumber': contact_phone,
            'priority': 0,  # Highest priority
            'metadata': {
                'alertType': alert_type,
                'location': location,
                'timestamp': datetime.utcnow().isoformat(),
                'urgency': 'critical',
                'requiresImmediate': True
            }
        },
        timeout=5
    )

    response.raise_for_status()
    data = response.json()

    # Log emergency call
    # (room id arrives via GET /api/v1/calls/{id} once the Dial queue places the call)
    log_emergency_call(
        call_id=data['data']['id'],
        status=data['data']['status'],
        alert_type=alert_type,
        location=location
    )

    return data['data']

Real-time Call Monitoring

async function initiateAndMonitor(agentId, phoneNumber) {
  // Initiate call
  const response = await fetch('https://v2.heppu.ai/api/v1/calls/outbound', {
    method: 'POST',
    headers: {
      'x-api-key': 'YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ agentId, phoneNumber })
  });

  const { data: call } = await response.json();
  console.log(`Call initiated: ${call.id}`);

  // Monitor call status
  const checkInterval = setInterval(async () => {
    const statusResponse = await fetch(
      `https://v2.heppu.ai/api/v1/calls/${call.id}`,
      { headers: { 'x-api-key': 'YOUR_API_KEY' } }
    );

    const { data: currentCall } = await statusResponse.json();

    console.log(`Status: ${currentCall.status}`);

    // Stop monitoring when call ends
    if (['completed', 'failed', 'cancelled'].includes(currentCall.status)) {
      clearInterval(checkInterval);
      console.log('Call ended');
      console.log(`Duration: ${currentCall.duration} seconds`);
      console.log(`Summary: ${currentCall.summary}`);
    }
  }, 5000); // Check every 5 seconds

  return call;
}

Webhook Triggered Call

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook/trigger-call', methods=['POST'])
def webhook_trigger_call():
    """Webhook endpoint to trigger outbound calls"""

    data = request.json

    # Validate webhook payload
    if not data.get('phoneNumber') or not data.get('agentId'):
        return jsonify({'error': 'Missing required fields'}), 400

    try:
        # Initiate call via Heppu API
        response = requests.post(
            'https://v2.heppu.ai/api/v1/calls/outbound',
            headers={'x-api-key': 'YOUR_API_KEY'},
            json={
                'agentId': data['agentId'],
                'phoneNumber': data['phoneNumber'],
                'priority': data.get('priority', 5),
                'metadata': {
                    'source': 'webhook',
                    'webhookId': data.get('id'),
                    **data.get('metadata', {})
                }
            },
            timeout=10
        )

        response.raise_for_status()
        call_data = response.json()

        return jsonify({
            'success': True,
            'callId': call_data['data']['id'],
            'status': call_data['data']['status']
        }), 201

    except requests.exceptions.RequestException as e:
        return jsonify({
            'success': False,
            'error': str(e)
        }), 500

Best Practices

  1. Use E.164 format - Always validate phone numbers before calling
  2. Handle agent status - Verify agent is active before initiating calls
  3. Set appropriate priority - Use priority 0 for urgent calls
  4. Include metadata - Store context for better call handling
  5. Implement monitoring - Poll call status for real-time updates
  6. Error handling - Always handle validation and API errors
  7. Rate limiting - Respect API rate limits for outbound calls
  8. Timeout handling - Set reasonable timeouts for API requests

Differences from Schedule Call

FeatureOutbound CallSchedule Call
ExecutionQueued to the Dial queue, dials near-instantly (CPS-paced)Queued for processing
Use CaseReal-time, manual callsAutomated, bulk calls
ResponseCall ID + "queued" statusCall scheduling confirmation
Status"queued""initiated" or "scheduled"
InfrastructureDial queue + LiveKit SDKQueue system

On this page