DEV Community

voipbin
voipbin

Posted on

Run AI Outbound Call Campaigns Without Managing Any Infrastructure

You've built an AI agent that holds smart conversations. Then your product manager says: "Great -- now let's use it to call 500 customers about their expiring subscriptions."

You start planning and quickly realize what you're actually signing up for:

  • Provisioning a SIP trunk (carrier agreements required)
  • Managing RTP media streams for every concurrent call
  • Implementing retry logic for busy/no-answer
  • Staying within carrier rate limits
  • Tracking call dispositions (answered, voicemail, failed)
  • Scaling the media server as concurrent calls grow

Before your AI has said a single word, you've built a telephony platform. And that's before you get to the business logic -- what the AI should say.


The Better Split: AI Owns the Conversation, Not the Infrastructure

VoIPBin is built around a simple idea: your AI should own the conversation logic, not the telephony plumbing. You call a REST API, VoIPBin places the call and handles RTP/TTS/STT, and you get a webhook when there's something to respond to.

Here's what a minimal outbound campaign looks like end to end.


Step 1: Get Your API Key

No credit card or OTP required. Signup is a single POST:

curl -s -X POST "https://api.voipbin.net/v1.0/auth/signup"   -H "Content-Type: application/json"   -d '{"username": "you@example.com", "password": "yourpassword"}'
Enter fullscreen mode Exit fullscreen mode

The response includes accesskey.token -- use it as your Bearer token from here on.


Step 2: Define Your Conversation Flow

A flow tells VoIPBin what to say and what to do with responses. Here is a subscription-renewal reminder:

{
  "name": "subscription-renewal-campaign",
  "actions": [
    {
      "type": "talk",
      "text": "Hi, this is Acme Corp. Your subscription expires in 3 days. Press 1 to renew now, press 2 for more options, or press 3 to be removed from this list."
    },
    {
      "type": "gather",
      "timeout": 5,
      "num_digits": 1,
      "action": "https://yourdomain.com/webhook/campaign-response"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Create the flow:

curl -s -X POST "https://api.voipbin.net/v1.0/flows"   -H "Authorization: Bearer YOUR_TOKEN"   -H "Content-Type: application/json"   -d @flow.json
Enter fullscreen mode Exit fullscreen mode

Save the returned flow_id -- you will use it when placing calls.


Step 3: Place Outbound Calls

With a flow ID, you can work through a contact list. Here is a Python script with basic rate limiting:

import urllib.request
import time
import json

API_BASE = "https://api.voipbin.net/v1.0"
TOKEN = "YOUR_ACCESS_TOKEN"
FLOW_ID = "YOUR_FLOW_ID"
FROM_NUMBER = "+15551234567"  # Your VoIPBin number

HEADERS = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

def place_call(to_number: str, contact_name: str) -> dict:
    payload = json.dumps({
        "flow_id": FLOW_ID,
        "source": FROM_NUMBER,
        "destinations": [{"type": "tel", "target": to_number}],
        "variables": {"contact_name": contact_name}
    }).encode()
    req = urllib.request.Request(
        f"{API_BASE}/calls",
        data=payload,
        headers=HEADERS,
        method="POST"
    )
    with urllib.request.urlopen(req) as resp:
        return json.load(resp)

def run_campaign(contacts: list[dict], calls_per_minute: int = 20):
    delay = 60.0 / calls_per_minute
    results = []
    for i, contact in enumerate(contacts):
        print(f"[{i+1}/{len(contacts)}] Calling {contact['name']} at {contact['phone']}")
        try:
            result = place_call(contact["phone"], contact["name"])
            results.append({"contact": contact, "call_id": result.get("id"), "status": "placed"})
        except Exception as e:
            results.append({"contact": contact, "call_id": None, "status": f"error: {e}"})
        time.sleep(delay)
    return results

contacts = [
    {"name": "Alice", "phone": "+15550001111"},
    {"name": "Bob",   "phone": "+15550002222"},
    {"name": "Carol", "phone": "+15550003333"},
]

results = run_campaign(contacts, calls_per_minute=20)
with open("campaign_results.json", "w") as f:
    json.dump(results, f, indent=2)
print(f"Campaign complete. {len(results)} calls placed.")
Enter fullscreen mode Exit fullscreen mode

Step 4: Handle Responses via Webhook

When a customer presses a key, VoIPBin calls your webhook. You reply with what happens next:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhook/campaign-response", methods=["POST"])
def handle_response():
    data = request.json
    digit = data.get("digit")
    call_id = data.get("call_id")

    if digit == "1":
        return jsonify({"actions": [
            {"type": "talk", "text": "Great! Transferring you to our renewal team."},
            {"type": "connect", "destination": "+15559998888"}
        ]})
    elif digit == "2":
        return jsonify({"actions": [
            {"type": "talk", "text": "You can also renew at acmecorp.com/renew. Goodbye!"},
            {"type": "hangup"}
        ]})
    elif digit == "3":
        remove_from_list(call_id)
        return jsonify({"actions": [
            {"type": "talk", "text": "You have been removed from our call list. Have a great day!"},
            {"type": "hangup"}
        ]})
    else:
        return jsonify({"actions": [
            {"type": "talk", "text": "We did not catch that. Please call 1-800-ACME. Goodbye!"},
            {"type": "hangup"}
        ]})

def remove_from_list(call_id):
    print(f"Removing contact for call {call_id}")

if __name__ == "__main__":
    app.run(port=3000)
Enter fullscreen mode Exit fullscreen mode

Notice what is missing: no SIP parsing, no audio encoding, no media state machine. Your webhook receives a clean event and returns a simple JSON array of next actions.


Step 5: Check Campaign Results

VoIPBin stores metadata for every call -- duration, disposition, and whether it was answered or went to voicemail:

curl -s "https://api.voipbin.net/v1.0/calls?flow_id=YOUR_FLOW_ID&page_size=100"   -H "Authorization: Bearer YOUR_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Each record includes:

  • status: completed, no_answer, busy, or failed
  • duration: in seconds
  • recording_id: if recording was enabled on the flow

What You Did Not Have to Build

Task DIY approach With VoIPBin
SIP trunk provisioning Your problem Done
RTP media handling Your problem Done
TTS / STT Integrate separately Built-in
Call disposition tracking Build your own schema Included
Scaling concurrent calls Provision servers Managed
Retry / no-answer logic Write it yourself Configurable

Your codebase stays focused on one thing: what the AI says and what happens in response.


Upgrading to Dynamic AI Conversations

Fixed scripts work well for simple reminders. But if you want your AI to respond dynamically -- handling questions, adapting tone, making real-time decisions -- VoIPBin supports connecting a live call to an AI agent via websocket. The infrastructure stays the same: VoIPBin handles the audio, your agent receives clean text and returns spoken responses.

This means you can start with a deterministic flow (step 2 above) and graduate to a fully conversational AI agent without rebuilding your campaign infrastructure.


Getting Started

  • Website: voipbin.net
  • Golang SDK: go get github.com/voipbin/voipbin-go
  • MCP server (Claude Desktop / Cursor): uvx voipbin-mcp
  • API base: https://api.voipbin.net/v1.0

Build the campaign logic. Let someone else run the phone system.

Top comments (0)