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:

ChannelHow it worksKey field
voice (default)The CampaignDialer places outbound calls through your telephony provider. The assigned AI agent handles the conversation.agent_id
smsThe SMSCampaignDialer sends text messages via Twilio. Each message is rendered from an sms_template with per-contact variable substitution.sms_template

Common Use Cases

2. Creating a Campaign

POST /api/campaigns

Required Fields

FieldTypeDescription
namestringHuman-readable campaign name.
agent_idUUIDThe AI agent that will handle calls.

Optional Fields

FieldTypeDefaultDescription
phone_number_idintegernullID of the phone number to call/send from. Required before launching.
max_concurrentinteger1Maximum simultaneous calls the dialer will place.
retry_attemptsinteger0How many times to retry a failed/no-answer contact.
retry_delay_minutesinteger60Minutes to wait between retry attempts.
scheduled_atISO 8601nullSchedule the campaign for a future date/time.
number_poolUUID[][]Array of phone_number IDs for round-robin caller ID rotation.
channelstring"voice""voice" or "sms".
sms_templatestringnullMessage 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

ColumnRequiredDescription
phone_numberYesContact phone number. Automatically normalized to E.164 format.
nameNoContact name.
emailNoContact email address.
notesNoFree-text notes.

Any column beyond the known fields is stored as a custom variable (see Contact Variables).

Phone Number Normalization

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"
You can upload multiple CSVs to the same campaign. Duplicate phone numbers are automatically skipped.
You cannot upload contacts to a campaign that is currently running. Pause or stop the campaign first.

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:

VariableSource
{{name}}Contact name column
{{email}}Contact email column
{{phone}}Contact phone number
If a variable placeholder has no matching value, it is left as-is in the output. Make sure your CSV column names match your template placeholders exactly.

5. Starting & Managing

Launch a Campaign

POST /api/campaigns/:id/launch

Before launching, the system verifies:

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

StatusDescription
draftCampaign created but not yet launched.
scheduledCampaign will start automatically at the scheduled_at time.
runningCampaign is actively placing calls or sending messages.
pausedDialing stopped. In-progress calls continue but no new calls are placed.
completedAll 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

  1. Checks how many calls are currently active vs. the max_concurrent limit
  2. Queries for contacts with status = 'pending', ordered by created_at ASC (FIFO)
  3. Pulls up to max_concurrent - activeCalls contacts
  4. Places a call for each contact, marks them as calling
  5. When a call completes, marks the contact as completed or failed
Each active call has a 5-minute timeout. If no completion webhook arrives within 5 minutes, the contact is automatically marked as 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
  }'
All phone numbers in the pool must be provisioned and active. Invalid or missing UUIDs are silently skipped.
A good rule of thumb: use at least 1 number per 20 contacts in your campaign.

7. Retry Logic

SettingDefaultDescription
retry_attempts0Maximum retry attempts per contact. 0 disables retries.
retry_delay_minutes60Minutes to wait before retrying.

Which Statuses Get Retried

StatusRetried?Reason
failedYesCall failed due to telephony error.
no_answerYesCall timed out with no pickup.
completedNoContact was successfully reached.
voicemailNoVoicemail was detected and handled.
Setting 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

BehaviorVoiceSMS
Concurrency modelUp to max_concurrent simultaneous callsRate-limited to maxPerSecond messages (default 1/sec)
ProviderConfigured telephony providerTwilio API directly
Opt-out handlingN/AContacts with opted_out: true are skipped
Cost trackingPer-call telephony costPer-segment SMS cost (160 chars/segment)
SMS messages longer than 160 characters are split into multiple segments. Keep templates concise to control costs.
SMS campaigns require Twilio credentials to be configured. The launch will fail if credentials are missing.

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

StatusMeaning
pendingWaiting to be dialed.
callingCall is currently in progress.
completedCall connected and finished successfully.
failedCall failed due to telephony error or timeout.
no_answerNo pickup within the 5-minute timeout.
voicemailVoicemail 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
)
Sending too many calls from a single number triggers carrier spam filtering. Pair high concurrency with a number pool.

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" }'
Always include the timezone offset in your scheduled_at value to avoid confusion.

12. Best Practices

  1. Start small. Test with 10–20 contacts before scaling up.
  2. Use number pools for larger campaigns. For campaigns with over 100 contacts, configure multiple outbound numbers.
  3. Set appropriate retry limits. 2–3 retries with 30–60 minute delay is the sweet spot.
  4. Monitor the first 5–10 calls. Watch for high failure rates early. Pause if something looks wrong.
  5. Respect TCPA and do-not-call regulations. Ensure you have prior express consent before placing automated calls.
  6. Schedule during business hours. Calls outside 8 AM–9 PM local time may violate regulations.
  7. Keep SMS templates under 160 characters when possible to avoid multi-segment billing.
  8. 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"
For a quick overview of all your campaigns, use GET /api/campaigns which returns every campaign with its agent name and phone number.