Outbound Campaigns Guide
Automate outbound voice and SMS campaigns to reach your contact lists at scale.
1. Overview
Outbound campaigns let you automatically dial (or text) a list of contacts using one of your configured AI agents. Instead of placing calls one at a time, you upload a CSV of contacts, assign an agent and caller ID, and launch the campaign. The platform handles concurrency, retries, number rotation, and status tracking for you.
Voice vs. SMS Campaigns
Every campaign has a channel field that determines how contacts are reached:
| Channel | How it works | Key field |
|---|---|---|
voice (default) | The CampaignDialer places outbound calls through your telephony provider. The assigned AI agent handles the conversation. | agent_id |
sms | The SMSCampaignDialer sends text messages via Twilio. Each message is rendered from an sms_template with per-contact variable substitution. | sms_template |
Common Use Cases
- Sales outreach — AI agent qualifies leads and books demos
- Appointment reminders — call or text patients/customers before scheduled visits
- Surveys & feedback — collect NPS or post-purchase feedback at scale
- Collections — automated payment reminder calls with escalation
- Event notifications — broadcast updates via SMS to a subscriber list
2. Creating a Campaign
POST /api/campaigns
Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Human-readable campaign name. |
agent_id | UUID | The AI agent that will handle calls. |
Optional Fields
| Field | Type | Default | Description |
|---|---|---|---|
phone_number_id | integer | null | ID of the phone number to call/send from. Required before launching. |
max_concurrent | integer | 1 | Maximum simultaneous calls the dialer will place. |
retry_attempts | integer | 0 | How many times to retry a failed/no-answer contact. |
retry_delay_minutes | integer | 60 | Minutes to wait between retry attempts. |
scheduled_at | ISO 8601 | null | Schedule the campaign for a future date/time. |
number_pool | UUID[] | [] | Array of phone_number IDs for round-robin caller ID rotation. |
channel | string | "voice" | "voice" or "sms". |
sms_template | string | null | Message template for SMS campaigns. |
Example: Create a Voice Campaign
curl -X POST https://your-domain.com/api/campaigns \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 Sales Outreach",
"agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone_number_id": 12,
"max_concurrent": 3,
"retry_attempts": 2,
"retry_delay_minutes": 30
}'
The response includes the new campaign with status: "draft". No calls are placed until you explicitly launch it.
3. Uploading Contacts
POST /api/campaigns/:id/upload-csv
Upload a CSV file as a multipart/form-data request with the field name file. Maximum file size is 5 MB. Only .csv files are accepted.
CSV Format
| Column | Required | Description |
|---|---|---|
phone_number | Yes | Contact phone number. Automatically normalized to E.164 format. |
name | No | Contact name. |
email | No | Contact email address. |
notes | No | Free-text notes. |
Any column beyond the known fields is stored as a custom variable (see Contact Variables).
Phone Number Normalization
5551234567(10 digits) becomes+1555123456715551234567(11 digits starting with 1) becomes+15551234567+15551234567is kept as-is- Numbers with fewer than 7 digits are rejected
Example CSV
phone_number,name,email,company,deal_stage,notes
+15551234567,John Doe,john@example.com,Acme Corp,proposal,VIP customer
5559876543,Jane Smith,jane@example.com,Beta Inc,discovery,Follow up on quote
(555) 555-0100,Bob Wilson,,Gamma LLC,lead,New lead from website
Upload Request
curl -X POST https://your-domain.com/api/campaigns/CAMPAIGN_ID/upload-csv \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@contacts.csv"
Download CSV Template
GET /api/campaigns/download-template
Returns a pre-formatted CSV template file with example data.
4. Contact Variables
Any CSV column that is not one of the known fields is stored in a variables JSONB column. This lets you pass arbitrary per-contact data into your agent's prompts.
How Variables Are Stored
Given this CSV row:
phone_number,name,email,company,deal_stage,appointment_date
+15551234567,John Doe,john@example.com,Acme Corp,proposal,2026-03-15
The variables JSONB becomes:
{
"company": "Acme Corp",
"deal_stage": "proposal",
"appointment_date": "2026-03-15"
}
Using Variables in Agent Prompts
Reference variables using {{variable_name}} syntax. The platform replaces placeholders before each call or SMS.
Hi {{name}}, this is Sarah from EWT. I'm calling about your {{deal_stage}} with {{company}}. Do you have a minute to chat?
Built-in variables available for every contact:
| Variable | Source |
|---|---|
{{name}} | Contact name column |
{{email}} | Contact email column |
{{phone}} | Contact phone number |
5. Starting & Managing
Launch a Campaign
POST /api/campaigns/:id/launch
Before launching, the system verifies:
- The campaign has an
agent_idassigned - The campaign has a
phone_number_idwith a valid phone number - There is at least one contact with
status = 'pending' - The campaign is not already running
curl -X POST https://your-domain.com/api/campaigns/CAMPAIGN_ID/launch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"max_concurrent": 5}'
Campaign States
| Status | Description |
|---|---|
draft | Campaign created but not yet launched. |
scheduled | Campaign will start automatically at the scheduled_at time. |
running | Campaign is actively placing calls or sending messages. |
paused | Dialing stopped. In-progress calls continue but no new calls are placed. |
completed | All contacts have been processed. |
Pause / Resume
POST /api/campaigns/:id/pause — Stops the dial loop.
POST /api/campaigns/:id/resume — Resumes dialing from where it left off.
How the Dialer Picks Contacts
- Checks how many calls are currently active vs. the
max_concurrentlimit - Queries for contacts with
status = 'pending', ordered bycreated_at ASC(FIFO) - Pulls up to
max_concurrent - activeCallscontacts - Places a call for each contact, marks them as
calling - When a call completes, marks the contact as
completedorfailed
no_answer.6. Number Pools
When running campaigns with many contacts, using a single caller ID number can trigger carrier-level spam blocking. Number pools solve this by rotating through multiple outbound numbers.
How It Works
The number_pool field is an array of phone_number UUIDs. The dialer uses round-robin selection: each successive call uses the next number in the pool.
curl -X POST https://your-domain.com/api/campaigns \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Large Outreach Campaign",
"agent_id": "AGENT_UUID",
"phone_number_id": 12,
"number_pool": [
"uuid-of-phone-1",
"uuid-of-phone-2",
"uuid-of-phone-3"
],
"max_concurrent": 5
}'
7. Retry Logic
| Setting | Default | Description |
|---|---|---|
retry_attempts | 0 | Maximum retry attempts per contact. 0 disables retries. |
retry_delay_minutes | 60 | Minutes to wait before retrying. |
Which Statuses Get Retried
| Status | Retried? | Reason |
|---|---|---|
failed | Yes | Call failed due to telephony error. |
no_answer | Yes | Call timed out with no pickup. |
completed | No | Contact was successfully reached. |
voicemail | No | Voicemail was detected and handled. |
retry_attempts too high can annoy contacts and trigger spam complaints. Keep retries to 2–3 maximum.8. SMS Campaigns
To create an SMS campaign, set channel to "sms" and provide an sms_template.
Creating an SMS Campaign
curl -X POST https://your-domain.com/api/campaigns \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Appointment Reminders",
"agent_id": "AGENT_UUID",
"phone_number_id": 12,
"channel": "sms",
"sms_template": "Hi {{name}}, this is a reminder about your appointment on {{appointment_date}} at {{appointment_time}}. Reply CONFIRM or call us at (555) 123-4567."
}'
How SMS Dialing Differs from Voice
| Behavior | Voice | SMS |
|---|---|---|
| Concurrency model | Up to max_concurrent simultaneous calls | Rate-limited to maxPerSecond messages (default 1/sec) |
| Provider | Configured telephony provider | Twilio API directly |
| Opt-out handling | N/A | Contacts with opted_out: true are skipped |
| Cost tracking | Per-call telephony cost | Per-segment SMS cost (160 chars/segment) |
9. Monitoring & Statistics
Campaign Stats
GET /api/campaigns/:id/stats
{
"campaign_id": "...",
"campaign_status": "running",
"contacts": {
"total": 150,
"pending": 87,
"calling": 3,
"completed": 48,
"failed": 7,
"no_answer": 4,
"voicemail": 1
},
"progress": 36,
"call_metrics": {
"avg_duration_seconds": 142,
"max_duration_seconds": 298,
"min_duration_seconds": 15,
"total_attempts": 65
}
}
Contact Status Values
| Status | Meaning |
|---|---|
pending | Waiting to be dialed. |
calling | Call is currently in progress. |
completed | Call connected and finished successfully. |
failed | Call failed due to telephony error or timeout. |
no_answer | No pickup within the 5-minute timeout. |
voicemail | Voicemail detected. |
List Individual Contacts
GET /api/campaigns/:id/contacts?page=1&limit=50&status=failed
Returns paginated contacts with individual status, attempt count, call duration, and error messages.
10. Rate Limiting
Outbound calling is constrained at two levels:
Organization-Level: outbound_rate_limit
Defaults to 10. Maximum concurrent outbound calls across all campaigns.
Campaign-Level: max_concurrent
Defaults to 1. Controls how many calls this specific campaign can have active simultaneously.
Agent-Level: max_concurrent_calls
Defaults to 0 (unlimited). Limits total concurrent calls for this agent across all campaigns.
How They Interact
effective_limit = min(
campaign.max_concurrent,
org.outbound_rate_limit,
agent.max_concurrent_calls || Infinity
)
11. Scheduling
Schedule a campaign to launch at a future date/time:
curl -X POST https://your-domain.com/api/campaigns/CAMPAIGN_ID/launch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "scheduled_at": "2026-03-01T09:00:00-05:00" }'
scheduled_at value to avoid confusion.12. Best Practices
- Start small. Test with 10–20 contacts before scaling up.
- Use number pools for larger campaigns. For campaigns with over 100 contacts, configure multiple outbound numbers.
- Set appropriate retry limits. 2–3 retries with 30–60 minute delay is the sweet spot.
- Monitor the first 5–10 calls. Watch for high failure rates early. Pause if something looks wrong.
- Respect TCPA and do-not-call regulations. Ensure you have prior express consent before placing automated calls.
- Schedule during business hours. Calls outside 8 AM–9 PM local time may violate regulations.
- Keep SMS templates under 160 characters when possible to avoid multi-segment billing.
- Use contact variables for personalization. Personalized first messages dramatically improve engagement.
13. End-to-End Example
Step 1: Create the Campaign
curl -X POST https://your-domain.com/api/campaigns \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Demo Follow-Up Calls",
"agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone_number_id": 12,
"max_concurrent": 3,
"retry_attempts": 2,
"retry_delay_minutes": 45,
"number_pool": [
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222",
"33333333-3333-3333-3333-333333333333"
]
}'
Step 2: Prepare and Upload Your CSV
phone_number,name,email,company,demo_date
+15551234567,Alice Johnson,alice@acme.com,Acme Corp,2026-02-18
+15559876543,Bob Martinez,bob@beta.io,Beta Inc,2026-02-19
+15555550100,Carol Lee,carol@gamma.co,Gamma LLC,2026-02-20
curl -X POST https://your-domain.com/api/campaigns/cmp_abc123/upload-csv \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@contacts.csv"
Step 3: Launch the Campaign
curl -X POST https://your-domain.com/api/campaigns/cmp_abc123/launch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
Step 4: Monitor Progress
# Check overall stats
curl https://your-domain.com/api/campaigns/cmp_abc123/stats \
-H "Authorization: Bearer YOUR_API_KEY"
# List failed contacts
curl "https://your-domain.com/api/campaigns/cmp_abc123/contacts?status=failed" \
-H "Authorization: Bearer YOUR_API_KEY"
# Pause if needed
curl -X POST https://your-domain.com/api/campaigns/cmp_abc123/pause \
-H "Authorization: Bearer YOUR_API_KEY"
# Resume when ready
curl -X POST https://your-domain.com/api/campaigns/cmp_abc123/resume \
-H "Authorization: Bearer YOUR_API_KEY"
Step 5: Review Results
curl https://your-domain.com/api/campaigns/cmp_abc123/stats \
-H "Authorization: Bearer YOUR_API_KEY"
/api/campaigns which returns every campaign with its agent name and phone number.