Make outbound calls and run campaigns

Updated

Have an agent call a phone number, or call a list of contacts on a schedule with a campaign.

Preview documentation

You're reading the Conversational AI Platform (Preview) docs. Switch to Main.

Your agent can call people from a phone number in your project. To place a single call, start a session that dials the number. To call a list of contacts, create a campaign that dials each contact on a schedule you set.

Understand the tech

An outbound call is a session that uses telephony transport instead of an RTC channel. The Platform dials the number from your phone number, and the agent talks to whoever answers. The session ends when the call ends, or when you stop it.

A campaign starts these sessions for you. You give it an agent, the number to call from, and a list of contacts. The campaign dials each contact as a separate session, within its schedule and pacing limits. It calls each contact once.

Prerequisites

Before you begin, make sure that you have:

Make an outbound call

Start a session with telephony transport. Set the number to call from, by its ID, and the number to call, in E.164 format. Pass the values for the agent's variables, as for any session.

The SDK's session helpers start RTC sessions only. To place a call, use the low-level client.v3.sessions.start():

session = client.v3.sessions.start(
    agent_id="3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
    values={"customer_name": "Ada"},
    transport={
        "type": "telephony",
        "from": "92e68758-3d3a-4f27-8b93-3fd8d834467c",
        "to": "+14155550123",
        "call_policy": {
            "max_ring_duration_ms": 30000,
            "max_call_duration_seconds": 600,
            "end_call": {"on_voicemail": True},
        },
    },
)
print(session["agent_session_id"])  # The agent_session_id

The SDK's session helpers start RTC sessions only. To place a call, use the low-level client.v3.sessions.start():

const session = await client.v3.sessions.start({
  agent_id: '3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10',
  values: { customer_name: 'Ada' },
  transport: {
    type: 'telephony',
    from: '92e68758-3d3a-4f27-8b93-3fd8d834467c',
    to: '+14155550123',
    call_policy: {
      max_ring_duration_ms: 30000,
      max_call_duration_seconds: 600,
      end_call: { on_voicemail: true },
    },
  },
});
console.log(session.agent_session_id); // The agent_session_id

The SDK's session helpers start RTC sessions only. To place a call, use the low-level client.V3.Sessions.Start(): This example also imports github.com/AgoraIO/agora-agents-go/v2/v3.

maxRing, maxCall := 30000, 600
session, err := client.V3.Sessions.Start(ctx, v3.SessionStartRequest{
	AgentID: "3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
	Values:  map[string]any{"customer_name": "Ada"},
	Transport: v3.SessionTransport{
		Type: "telephony",
		From: "92e68758-3d3a-4f27-8b93-3fd8d834467c",
		To:   "+14155550123",
		CallPolicy: &v3.CampaignCallPolicy{
			MaxRingDurationMS:      &maxRing,
			MaxCallDurationSeconds: &maxCall,
			EndCall:                &v3.CampaignEndCallPolicy{OnVoicemail: true},
		},
	},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(session.Body.AgentSessionID) // The agent_session_id

Send a request to Start a session. In transport, set type to telephony, from to your number's number_id, and to to the number to call. from doesn't accept a phone number:

curl --request POST \
  --url https://preview.ai.agora.io/conversational-ai/v1/projects/<your_app_id>/sessions \
  --header 'Authorization: Basic <your_base64_encoded_credentials>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "agent_id": "3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
  "values": {
    "customer_name": "Ada"
  },
  "transport": {
    "type": "telephony",
    "from": "92e68758-3d3a-4f27-8b93-3fd8d834467c",
    "to": "+14155550123",
    "call_policy": {
      "max_ring_duration_ms": 30000,
      "max_call_duration_seconds": 600,
      "end_call": {
        "on_voicemail": true
      }
    }
  }
}'

The response contains the session ID and its status, as for an RTC session. A session status of RUNNING doesn't mean that the call was answered. To end the call, stop the session.

Set call limits and behavior

In the call policy, set limits and behavior for the call. If you omit a limit, it doesn't apply.

FieldDescription
max_ring_duration_msThe maximum time in milliseconds from starting the dial until the call is answered. If nobody answers in time, the dial attempt ends.
max_call_duration_secondsThe maximum length of the call in seconds, from when it's answered. The session's lifecycle limits can end the call earlier.
max_silence_duration_msHang up when nobody on the call has spoken for this many milliseconds. In a campaign, the minimum is 30000.
end_callThe conditions under which the agent can end the call itself. Each condition is off unless you set it to true. on_user_request: the caller clearly says goodbye or asks to hang up. on_voicemail: the call reaches voicemail. on_ai_assistant: an AI assistant or another automated system answers.

Run a campaign

Create a campaign with the agent, the number to call from, the contacts, and a schedule. For each contact, pass the phone number and the values for the agent's variables.

The following example calls customers on weekday mornings and afternoons in New York time for two weeks:

Pass the number to call from as a list.

ranges = [{"start": "09:00", "end": "12:00"}, {"start": "13:00", "end": "17:00"}]

campaign = client.campaigns.create(
    name="november-appointments",
    agent_id="3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
    transport={
        "type": "telephony",
        "from": ["92e68758-3d3a-4f27-8b93-3fd8d834467c"],
        "call_policy": {"max_ring_duration_ms": 30000, "end_call": {"on_voicemail": True}},
    },
    contacts=[
        {"phone": "+12025550123", "values": {"customer_name": "Ava", "plan": "pro"}},
        {"phone": "+12025550124", "values": {"customer_name": "Ben", "plan": "basic"}},
    ],
    schedule={
        "mode": "scheduled",
        "timezone": "America/New_York",
        "start": "2026-11-02T09:00",
        "end": "2026-11-13T17:00",
        "days": [{"weekday": day, "ranges": ranges} for day in ("mon", "tue", "wed", "thu", "fri")],
    },
    pacing={"max_concurrent": 10},
)
print(campaign["campaign_id"], campaign["status"])

Pass the number to call from as a list.

const ranges = [
  { start: '09:00', end: '12:00' },
  { start: '13:00', end: '17:00' },
];

const campaign = await client.campaigns.create({
  name: 'november-appointments',
  agent_id: '3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10',
  transport: {
    type: 'telephony',
    from: ['92e68758-3d3a-4f27-8b93-3fd8d834467c'],
    call_policy: { max_ring_duration_ms: 30000, end_call: { on_voicemail: true } },
  },
  contacts: [
    { phone: '+12025550123', values: { customer_name: 'Ava', plan: 'pro' } },
    { phone: '+12025550124', values: { customer_name: 'Ben', plan: 'basic' } },
  ],
  schedule: {
    mode: 'scheduled',
    timezone: 'America/New_York',
    start: '2026-11-02T09:00',
    end: '2026-11-13T17:00',
    days: (['mon', 'tue', 'wed', 'thu', 'fri'] as const).map((weekday) => ({ weekday, ranges })),
  },
  pacing: { max_concurrent: 10 },
});
console.log(campaign.campaign_id, campaign.status);

Pass the number to call from as a list. This example also imports github.com/AgoraIO/agora-agents-go/v2/v3.

ranges := []v3.CampaignTimeRange{{Start: "09:00", End: "12:00"}, {Start: "13:00", End: "17:00"}}
var days []v3.CampaignCallingDay
for _, weekday := range []string{"mon", "tue", "wed", "thu", "fri"} {
	days = append(days, v3.CampaignCallingDay{Weekday: weekday, Ranges: ranges})
}
maxRing, maxConcurrent := 30000, 10

campaign, err := client.Campaigns.Create(ctx, v3.CampaignCreate{
	Name:    "november-appointments",
	AgentID: "3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
	Transport: v3.CampaignTransport{
		Type: "telephony",
		From: []string{"92e68758-3d3a-4f27-8b93-3fd8d834467c"},
		CallPolicy: &v3.CampaignCallPolicy{
			MaxRingDurationMS: &maxRing,
			EndCall:           &v3.CampaignEndCallPolicy{OnVoicemail: true},
		},
	},
	Contacts: []v3.CampaignContact{
		{Phone: "+12025550123", Values: map[string]any{"customer_name": "Ava", "plan": "pro"}},
		{Phone: "+12025550124", Values: map[string]any{"customer_name": "Ben", "plan": "basic"}},
	},
	Schedule: v3.CampaignSchedule{
		Mode:     "scheduled",
		Timezone: "America/New_York",
		Start:    "2026-11-02T09:00",
		End:      "2026-11-13T17:00",
		Days:     days,
	},
	Pacing: &v3.CampaignPacing{MaxConcurrent: &maxConcurrent},
}, v3.CampaignWriteOptions{})
if err != nil {
	log.Fatal(err)
}
fmt.Println(campaign.Body.CampaignID, campaign.Body.Status)

Send a request to Create a campaign:

curl --request POST \
  --url https://preview.ai.agora.io/conversational-ai/v1/projects/<your_app_id>/campaigns \
  --header 'Authorization: Basic <your_base64_encoded_credentials>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "name": "november-appointments",
  "agent_id": "3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
  "transport": {
    "type": "telephony",
    "from": "92e68758-3d3a-4f27-8b93-3fd8d834467c",
    "call_policy": {
      "max_ring_duration_ms": 30000,
      "end_call": {
        "on_voicemail": true
      }
    }
  },
  "contacts": [
    {
      "phone": "+12025550123",
      "values": { "customer_name": "Ava", "plan": "pro" }
    },
    {
      "phone": "+12025550124",
      "values": { "customer_name": "Ben", "plan": "basic" }
    }
  ],
  "schedule": {
    "mode": "scheduled",
    "timezone": "America/New_York",
    "start": "2026-11-02T09:00",
    "end": "2026-11-13T17:00",
    "days": [
      { "weekday": "mon", "ranges": [{ "start": "09:00", "end": "12:00" }, { "start": "13:00", "end": "17:00" }] },
      { "weekday": "tue", "ranges": [{ "start": "09:00", "end": "12:00" }, { "start": "13:00", "end": "17:00" }] },
      { "weekday": "wed", "ranges": [{ "start": "09:00", "end": "12:00" }, { "start": "13:00", "end": "17:00" }] },
      { "weekday": "thu", "ranges": [{ "start": "09:00", "end": "12:00" }, { "start": "13:00", "end": "17:00" }] },
      { "weekday": "fri", "ranges": [{ "start": "09:00", "end": "12:00" }, { "start": "13:00", "end": "17:00" }] }
    ]
  },
  "pacing": {
    "max_concurrent": 10
  }
}'

You can pass up to 50,000 contacts, and the whole request body can be at most 10 MiB. You can't change the contacts after you create the campaign. A phone number that appears more than once is called once for each entry. If any contact is missing a value for a required variable, or has a value that doesn't match the variable's type or options, the whole request fails with 400, and no campaign is created.

If the request is successful, the response contains the campaign, including its ID and status:

// 201 Created
{
  "campaign_id": "b4f0581e-f138-43a2-851c-a8f8d228bb37",
  "name": "november-appointments",
  "status": "scheduled",
  "agent_id": "3f6c2a9e-8d1b-4e7a-9c55-0b2f7d1e4a10",
  "transport": {
    "type": "telephony",
    "from": ["92e68758-3d3a-4f27-8b93-3fd8d834467c"],
    "call_policy": {
      "max_ring_duration_ms": 30000,
      "end_call": {
        "on_voicemail": true
      }
    }
  },
  "lifecycle": {
    "idle_timeout_ms": 30000,
    "max_duration_ms": 300000,
    "graceful_stop": {
      "enabled": false,
      "timeout_ms": 30000
    }
  },
  "schedule": { ... },  // The schedule you set in the request.
  "counts": {
    "contacts": 2,
    "pending": 2,
    "calling": 0,
    "completed": 0,
    "failed": 0,
    "canceled": 0
  },
  "create_ts": 1787673602,
  "start_ts": null,
  "complete_ts": null,
  "failure": null
}

The response returns transport.from as a list, and fills in the default lifecycle values for the settings you didn't pass.

You can't change the contacts after you create the campaign. A phone number that appears more than once is called once for each entry. If any contact is missing a value for a required variable, or has a value that doesn't match the variable's type or options, the whole request fails with 400, and no campaign is created.

Store the campaign ID. You use it to check and control the campaign.

Set the schedule

In the schedule, set mode to one of the following:

  • immediate: The campaign starts dialing as soon as you create it. Don't pass start or end.
  • scheduled: The campaign dials from start until end. Pass both as local dates and times in YYYY-MM-DDTHH:MM format, and set timezone to an IANA time zone, such as America/New_York.

To limit dialing to certain hours, add days. For each weekday, pass up to 10 time ranges in 24-hour HH:MM format. Outside these hours, no new calls start. A day you don't list has no dialing. If you omit days, the campaign dials at any time between start and end.

Set the pacing

In the pacing settings, control how fast the campaign dials. The campaign calls each contact once. Automatic retries aren't available in the Preview.

FieldDescription
max_concurrentThe maximum number of calls in progress at the same time, from 1 to 10,000. The default is 10.
max_calls_per_secondThe maximum number of new dials in any one-second window, up to 1,000. If you omit it, the campaign uses your project's default dial rate.

Monitor and control a campaign

To check a campaign's progress, retrieve it. The response contains its status and counts, the number of contacts that are pending, calling, completed, failed, or canceled.

campaign = client.campaigns.get(campaign_id="<campaign_id>")
print(campaign["status"], campaign["counts"])
const campaign = await client.campaigns.get({ id: '<campaign_id>' });
console.log(campaign.status, campaign.counts);
campaign, err := client.Campaigns.Get(ctx, "<campaign_id>")
if err != nil {
	log.Fatal(err)
}
fmt.Println(campaign.Body.Status, campaign.Body.Counts)

Send a request to Retrieve a campaign:

curl --request GET \
  --url https://preview.ai.agora.io/conversational-ai/v1/projects/<your_app_id>/campaigns/<campaign_id> \
  --header 'Authorization: Basic <your_base64_encoded_credentials>'

A campaign has one of the following statuses:

StatusDescription
scheduledThe campaign is waiting for its start time.
runningThe campaign is dialing, or can dial during its schedule.
pausedYou paused the campaign.
completedThe campaign has finished.
canceledYou canceled the campaign.
failedThe campaign failed. The details are in failure.

To control a running campaign, use one of the following operations:

  • Pause a campaign: Stops new calls. Calls in progress continue.
  • Resume a campaign: Continues dialing a paused campaign.
  • Cancel a campaign: Ends the campaign permanently. Contacts that haven't been called change to canceled, and you can't resume the campaign.

For example, to pause a campaign:

client.campaigns.pause(campaign_id="<campaign_id>")
await client.campaigns.pause({ id: '<campaign_id>' });
if _, err := client.Campaigns.Pause(ctx, "<campaign_id>"); err != nil {
	log.Fatal(err)
}
curl --request POST \
  --url 'https://preview.ai.agora.io/conversational-ai/v1/projects/<your_app_id>/campaigns/<campaign_id>:pause' \
  --header 'Authorization: Basic <your_base64_encoded_credentials>'

To change a campaign, update it. Before the campaign starts dialing, you can change pacing, schedule, lifecycle, and transport.call_policy. After it starts, you can change only pacing.

Reference

For more information, see the following pages:

API reference