DEV Community

Sonam
Sonam

Posted on

Build an AI Outbound Call Campaign with Python and Telnyx

Outbound call campaigns look simple until you need them to behave like real software.

Calling one number is easy. Calling a list of numbers means you suddenly care about queueing, rate limits, call state, webhook verification, result tracking, and summaries for the team.

I built a small example for that pattern: an AI Call Campaign Orchestrator using Python, Flask, Telnyx Call Control, the Telnyx Agent SDK, SQLite, and SMS.

Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/ai-call-campaign-orchestrator

What the app does

The app exposes a simple API:

  • POST /campaign starts a campaign from a list of phone numbers
  • GET /campaign/<campaign_id> returns campaign status and call results
  • POST /webhooks/call receives Telnyx Call Control events
  • GET /health confirms the service is running

The interesting part is what happens after you submit the campaign.

The CampaignAgent queues the calls, applies a rate limit, places outbound calls through Call Control, stores results in SQLite, and sends an SMS summary when the campaign is complete.

POST /campaign
  -> queue calls
  -> rate-limit delivery
  -> place calls with Call Control
  -> receive signed webhooks
  -> store results in SQLite
  -> send SMS summary
Enter fullscreen mode Exit fullscreen mode

Starting a campaign

You send a list of E.164 phone numbers to POST /campaign:

curl -X POST http://localhost:5000/campaign \
  -H "Content-Type: application/json" \
  -d '{
    "phone_numbers": [
      "+15551234567",
      "+15557654321",
      "+15559876543"
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

The API returns a campaign ID:

{
  "campaign_id": "3f2c1a5e-8b7d-4e6f-9a0c-1d2e3f4a5b6c",
  "status": "started"
}
Enter fullscreen mode Exit fullscreen mode

From there, the agent owns the campaign lifecycle.

Why rate limiting matters

Outbound communication workflows need pacing.

If you fire too many calls at once, you can create a bad user experience and run into carrier or operational limits. This sample uses CALL_RATE_LIMIT_PER_MINUTE so campaign delivery is controlled from configuration instead of buried inside the call loop.

That makes the campaign behavior easier to tune without rewriting the app.

Placing calls with Call Control

For each queued number, the app places an outbound Call Control call with the Telnyx Python SDK.

The app needs:

  • TELNYX_API_KEY
  • TELNYX_CONNECTION_ID
  • TELNYX_PHONE_NUMBER
  • TELNYX_WEBHOOK_URL

The webhook URL points back to the app, so call lifecycle events can update campaign state as calls are answered, completed, or fail.

Verifying webhooks

The webhook endpoint uses telnyx.webhooks.unwrap() with the Telnyx Ed25519 public key.

That matters because webhook handlers should not blindly trust incoming HTTP requests. The app verifies that the event really came from Telnyx before it updates call state.

Telnyx Call Control event
  -> POST /webhooks/call
  -> verify signature
  -> update campaign result
Enter fullscreen mode Exit fullscreen mode

Tracking status

At any point, you can poll:

curl http://localhost:5000/campaign/<campaign_id>
Enter fullscreen mode Exit fullscreen mode

The response includes total calls, completion status, and per-number results.

That gives you a simple campaign dashboard API without adding another database service just for the sample.

Closing the loop with SMS

When the campaign finishes, the app sends a summary SMS using Telnyx Messaging.

You configure:

  • TELNYX_SMS_FROM
  • TELNYX_SMS_TO

For a demo, this is a clean way to prove the workflow completed. In a production version, the same step could also notify Slack, update a CRM, or send a report to another internal system.

What I like about this pattern

The app is not trying to be a full campaign product.

It is showing the backend shape you need for one:

  • one API to start the work
  • an agent to manage queueing and pacing
  • Call Control for the voice workflow
  • signed webhooks for lifecycle updates
  • SQL for campaign state
  • SMS for completion notification

That is the useful part. It gives you the skeleton for a durable communications workflow without starting from a pile of disconnected services.

Run it yourself

Clone the repo:

git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/ai-call-campaign-orchestrator
Enter fullscreen mode Exit fullscreen mode

Create a virtual environment and install dependencies:

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Configure your environment:

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Then fill in your Telnyx API key, connection ID, phone number, webhook URL, public key, SMS settings, and call rate limit.

Run the app:

python app.py
Enter fullscreen mode Exit fullscreen mode

Resources:

Top comments (0)