Bulk Import Leads
Use the lead loader APIs to add leads to an outbound campaign. RingCX supports direct JSON loading and a preview/process flow for uploaded files.
Rate Limiting & Stability
Lead loader requests are throttled per RingCX sub-account. Direct lead loading is limited to 30 requests per 10 seconds and 120 requests per 60 seconds. File preview is limited to 12 requests per 30 seconds and 18 requests per 60 seconds. File processing is limited to 9 requests per 3 seconds and 30 requests per 30 seconds. Batch leads into fewer uploads where practical, and implement exponential backoff on 429 Too Many Requests responses.
SDK Setup
SDK examples in this article use JWT authentication and load credentials from environment variables.
npm install ringcentral-engage-voice-client dotenv
pip3 install ringcentral_engage_voice python-dotenv
Create a .env file in the directory where you run the sample:
RC_CLIENT_ID=<clientId>
RC_CLIENT_SECRET=<clientSecret>
RC_JWT=<jwt>
The SDK wrapper reads these values, signs in with RingCentral, and exchanges the RingCentral access token for a RingCX access token before calling RingCX APIs.
Find the Campaign
Before loading leads, identify the target campaign:
GET https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{accountId}/dialGroups
Authorization: Bearer <ringcxAccessToken>
Accept: application/json
GET https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{accountId}/dialGroups/{dialGroupId}/campaigns
Authorization: Bearer <ringcxAccessToken>
Accept: application/json
import requests
account_id = "<accountId>"
dial_group_id = "<dialGroupId>"
access_token = "<ringcxAccessToken>"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
}
dial_groups = requests.get(
f"https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{account_id}/dialGroups",
headers=headers,
)
dial_groups.raise_for_status()
campaigns = requests.get(
f"https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{account_id}/dialGroups/{dial_group_id}/campaigns",
headers=headers,
)
campaigns.raise_for_status()
print(dial_groups.json())
print(campaigns.json())
const accountId = "<accountId>";
const dialGroupId = "<dialGroupId>";
const accessToken = "<ringcxAccessToken>";
const headers = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
};
const dialGroups = await fetch(
`https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/${accountId}/dialGroups`,
{ headers }
);
if (!dialGroups.ok) throw new Error(await dialGroups.text());
const campaigns = await fetch(
`https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/${accountId}/dialGroups/${dialGroupId}/campaigns`,
{ headers }
);
if (!campaigns.ok) throw new Error(await campaigns.text());
console.log(await dialGroups.json());
console.log(await campaigns.json());
const EngageVoice = require("ringcentral-engage-voice-client").default;
require("dotenv").config();
async function main() {
const ev = new EngageVoice({
clientId: process.env.RC_CLIENT_ID,
clientSecret: process.env.RC_CLIENT_SECRET
});
await ev.authorize({ jwt: process.env.RC_JWT });
const dialGroups = await ev.get(
"/api/v1/admin/accounts/{accountId}/dialGroups"
);
const campaigns = await ev.get(
"/api/v1/admin/accounts/{accountId}/dialGroups/{dialGroupId}/campaigns"
);
console.log(dialGroups.data);
console.log(campaigns.data);
}
main().catch(console.error);
import os
from dotenv import load_dotenv
from ringcentral_engage_voice import RingCentralEngageVoice
load_dotenv()
ev = RingCentralEngageVoice(
os.environ["RC_CLIENT_ID"],
os.environ["RC_CLIENT_SECRET"],
)
ev.authorize(jwt=os.environ["RC_JWT"])
dial_groups = ev.get(
"/api/v1/admin/accounts/{accountId}/dialGroups"
).json()
campaigns = ev.get(
"/api/v1/admin/accounts/{accountId}/dialGroups/{dialGroupId}/campaigns"
).json()
print(dial_groups)
print(campaigns)
Use the returned campaignId in the lead loader path.
Choose an Import Method
| Method | Use when | Endpoint |
|---|---|---|
| Direct lead loading | Your integration already has structured lead records and can send them as JSON. | POST /voice/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/direct |
| File preview and process | Users upload a CSV, Excel, pipe-delimited, or tab-delimited file and need to map file columns before loading. | POST /voice/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/preview, then POST /voice/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/process |
Direct Lead Loading
Direct loading sends leads as JSON and is the simplest option when your integration already has structured lead data.
POST https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/direct
Authorization: Bearer <ringcxAccessToken>
Content-Type: application/json
{
"description": "Renewal leads",
"dialPriority": "NORMAL",
"duplicateHandling": "REMOVE_FROM_LIST",
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"uploadLeads": [
{
"externId": "lead-1001",
"leadPhone": "4155550100",
"leadPhoneE164": "+14155550100",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"state": "CA",
"zip": "94105",
"leadPriority": 5,
"maxPasses": 3,
"callerId": "4155550199",
"auxData1": "renewal"
}
]
}
import requests
account_id = "<accountId>"
campaign_id = "<campaignId>"
access_token = "<ringcxAccessToken>"
payload = {
"description": "Renewal leads",
"dialPriority": "NORMAL",
"duplicateHandling": "REMOVE_FROM_LIST",
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"uploadLeads": [
{
"externId": "lead-1001",
"leadPhone": "4155550100",
"leadPhoneE164": "+14155550100",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"state": "CA",
"zip": "94105",
"leadPriority": 5,
"maxPasses": 3,
"callerId": "4155550199",
"auxData1": "renewal",
}
],
}
response = requests.post(
f"https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{account_id}/campaigns/{campaign_id}/leadLoader/direct",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json=payload,
)
response.raise_for_status()
print(response.json())
const accountId = "<accountId>";
const campaignId = "<campaignId>";
const accessToken = "<ringcxAccessToken>";
const payload = {
description: "Renewal leads",
dialPriority: "NORMAL",
duplicateHandling: "REMOVE_FROM_LIST",
listState: "ACTIVE",
timeZoneOption: "NOT_APPLICABLE",
uploadLeads: [
{
externId: "lead-1001",
leadPhone: "4155550100",
leadPhoneE164: "+14155550100",
firstName: "Ada",
lastName: "Lovelace",
email: "[email protected]",
state: "CA",
zip: "94105",
leadPriority: 5,
maxPasses: 3,
callerId: "4155550199",
auxData1: "renewal"
}
]
};
const response = await fetch(
`https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/${accountId}/campaigns/${campaignId}/leadLoader/direct`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
}
);
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
const EngageVoice = require("ringcentral-engage-voice-client").default;
require("dotenv").config();
async function main() {
const ev = new EngageVoice({
clientId: process.env.RC_CLIENT_ID,
clientSecret: process.env.RC_CLIENT_SECRET
});
await ev.authorize({ jwt: process.env.RC_JWT });
const payload = {
description: "Renewal leads",
dialPriority: "NORMAL",
duplicateHandling: "REMOVE_FROM_LIST",
listState: "ACTIVE",
timeZoneOption: "NOT_APPLICABLE",
uploadLeads: [
{
externId: "lead-1001",
leadPhone: "4155550100",
leadPhoneE164: "+14155550100",
firstName: "Ada",
lastName: "Lovelace",
email: "[email protected]",
state: "CA",
zip: "94105",
leadPriority: 5,
maxPasses: 3,
callerId: "4155550199",
auxData1: "renewal"
}
]
};
const response = await ev.post(
"/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/direct",
payload
);
console.log(response.data);
}
main().catch(console.error);
import os
from dotenv import load_dotenv
from ringcentral_engage_voice import RingCentralEngageVoice
load_dotenv()
ev = RingCentralEngageVoice(
os.environ["RC_CLIENT_ID"],
os.environ["RC_CLIENT_SECRET"],
)
ev.authorize(jwt=os.environ["RC_JWT"])
payload = {
"description": "Renewal leads",
"dialPriority": "NORMAL",
"duplicateHandling": "REMOVE_FROM_LIST",
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"uploadLeads": [
{
"externId": "lead-1001",
"leadPhone": "4155550100",
"leadPhoneE164": "+14155550100",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "[email protected]",
"state": "CA",
"zip": "94105",
"leadPriority": 5,
"maxPasses": 3,
"callerId": "4155550199",
"auxData1": "renewal",
}
],
}
response = ev.post(
"/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/direct",
payload,
).json()
print(response)
Direct Load Response
The direct load response summarizes accepted, inserted, converted, rejected, and DNC-affected leads.
{
"leadsSupplied": 1,
"leadsAccepted": 1,
"leadsInserted": 1,
"leadsConverted": 1,
"dncReturnedCount": 0,
"failedAgentAssignment": 0,
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"processingResult": "OK",
"rejectedRows": []
}
File Preview and Process
For file-based imports, preview the file first so RingCX can identify columns and return mapping information. Then submit the process request with the selected mapping.
The pageColumnMappings values are zero-based column indexes from the selected preview page. For example, if the preview shows LEAD_PHONE in the first column, map LEAD_PHONE to 0.
POST https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/preview?fileType=COMMA
Authorization: Bearer <ringcxAccessToken>
Content-Type: multipart/form-data
[email protected]
POST https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/process
Authorization: Bearer <ringcxAccessToken>
Content-Type: application/json
{
"transactionId": "<transactionId>",
"description": "Renewal leads",
"fileType": "COMMA",
"fileContainsHeaders": true,
"duplicateHandling": "REMOVE_FROM_LIST",
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"pageNumber": 1,
"pageColumnMappings": {
"LEAD_PHONE": 0,
"EXTERN_ID": 1,
"FIRST_NAME": 2,
"LAST_NAME": 3
}
}
import requests
account_id = "<accountId>"
campaign_id = "<campaignId>"
access_token = "<ringcxAccessToken>"
headers = {"Authorization": f"Bearer {access_token}"}
with open("leads.csv", "rb") as leads_file:
preview = requests.post(
f"https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{account_id}/campaigns/{campaign_id}/leadLoader/preview",
params={"fileType": "COMMA"},
headers=headers,
files={"file": leads_file},
)
preview.raise_for_status()
preview_body = preview.json()
process_payload = {
"transactionId": preview_body["transactionId"],
"description": "Renewal leads",
"fileType": "COMMA",
"fileContainsHeaders": True,
"duplicateHandling": "REMOVE_FROM_LIST",
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"pageNumber": 1,
"pageColumnMappings": {
"LEAD_PHONE": 0,
"EXTERN_ID": 1,
"FIRST_NAME": 2,
"LAST_NAME": 3,
},
}
process = requests.post(
f"https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/{account_id}/campaigns/{campaign_id}/leadLoader/process",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json=process_payload,
)
process.raise_for_status()
print(f"Lead file processing accepted: {process.status_code}")
import { readFile } from "node:fs/promises";
const accountId = "<accountId>";
const campaignId = "<campaignId>";
const accessToken = "<ringcxAccessToken>";
const formData = new FormData();
formData.append("file", new Blob([await readFile("leads.csv")]), "leads.csv");
const preview = await fetch(
`https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/${accountId}/campaigns/${campaignId}/leadLoader/preview?fileType=COMMA`,
{
method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
body: formData
}
);
if (!preview.ok) throw new Error(await preview.text());
const previewBody = await preview.json();
const processPayload = {
transactionId: previewBody.transactionId,
description: "Renewal leads",
fileType: "COMMA",
fileContainsHeaders: true,
duplicateHandling: "REMOVE_FROM_LIST",
listState: "ACTIVE",
timeZoneOption: "NOT_APPLICABLE",
pageNumber: 1,
pageColumnMappings: {
LEAD_PHONE: 0,
EXTERN_ID: 1,
FIRST_NAME: 2,
LAST_NAME: 3
}
};
const process = await fetch(
`https://ringcx.ringcentral.com/voice/api/v1/admin/accounts/${accountId}/campaigns/${campaignId}/leadLoader/process`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(processPayload)
}
);
if (!process.ok) throw new Error(await process.text());
console.log(`Lead file processing accepted: ${process.status}`);
After the preview response returns a transactionId, you can submit the process request with the SDK:
const EngageVoice = require("ringcentral-engage-voice-client").default;
require("dotenv").config();
async function main() {
const ev = new EngageVoice({
clientId: process.env.RC_CLIENT_ID,
clientSecret: process.env.RC_CLIENT_SECRET
});
await ev.authorize({ jwt: process.env.RC_JWT });
const payload = {
transactionId: "<transactionId>",
description: "Renewal leads",
fileType: "COMMA",
fileContainsHeaders: true,
duplicateHandling: "REMOVE_FROM_LIST",
listState: "ACTIVE",
timeZoneOption: "NOT_APPLICABLE",
pageNumber: 1,
pageColumnMappings: {
LEAD_PHONE: 0,
EXTERN_ID: 1,
FIRST_NAME: 2,
LAST_NAME: 3
}
};
const response = await ev.post(
"/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/process",
payload
);
console.log(response.data);
}
main().catch(console.error);
import os
from dotenv import load_dotenv
from ringcentral_engage_voice import RingCentralEngageVoice
load_dotenv()
ev = RingCentralEngageVoice(
os.environ["RC_CLIENT_ID"],
os.environ["RC_CLIENT_SECRET"],
)
ev.authorize(jwt=os.environ["RC_JWT"])
payload = {
"transactionId": "<transactionId>",
"description": "Renewal leads",
"fileType": "COMMA",
"fileContainsHeaders": True,
"duplicateHandling": "REMOVE_FROM_LIST",
"listState": "ACTIVE",
"timeZoneOption": "NOT_APPLICABLE",
"pageNumber": 1,
"pageColumnMappings": {
"LEAD_PHONE": 0,
"EXTERN_ID": 1,
"FIRST_NAME": 2,
"LAST_NAME": 3,
},
}
response = ev.post(
"/api/v1/admin/accounts/{accountId}/campaigns/{campaignId}/leadLoader/process",
payload,
).json()
print(response)
Preview Response
The preview response includes the transactionId required by the process request, the mapping columns supported by the loader, and sample rows for each preview page.
{
"transactionId": "8c7406f8-31cf-4f6a-a5a4-7a6b25e35f52",
"mappingColumns": [
"LEAD_PHONE",
"EXTERN_ID",
"FIRST_NAME",
"LAST_NAME"
],
"pagePreviews": [
{
"pageNumber": 1,
"pageName": "leads.csv",
"rowData": [
["phone", "external_id", "first_name", "last_name"],
["4155550100", "lead-1001", "Ada", "Lovelace"]
]
}
]
}
The process request returns an accepted or created status after RingCX accepts the file for processing.
Common Fields
| Field | Description |
|---|---|
description |
Name or description for the uploaded lead list. |
uploadLeads |
Array of lead records for direct loading. |
externId |
External lead identifier from your source system. |
leadPhone |
Primary lead phone number. |
leadPhoneE164 |
E.164 primary phone number, when required for the account mode. |
email, firstName, lastName, state, zip |
Standard contact fields that can be stored with each lead. |
leadPriority |
Numeric priority used by outbound dialing when prioritization is enabled. |
maxPasses |
Maximum number of dialing passes for the lead. |
callerId |
Caller ID to use for the lead when the campaign supports lead-level caller ID. |
auxData1 through auxData5 |
Custom fields for campaign- or integration-specific lead data. |
duplicateHandling |
Duplicate behavior during load. Values include RETAIN_ALL, REMOVE_ALL_EXISTING, and REMOVE_FROM_LIST. |
timeZoneOption |
How RingCX derives or applies lead time zones. Values include NPA_NXX, ZIPCODE, EXPLICIT, COUNTRY, and NOT_APPLICABLE. |
fileType |
File format for preview/process imports. Values are EXCEL, PIPE, COMMA, and TAB. |
pageColumnMappings |
Map of RingCX lead fields to zero-based column indexes from the preview response. |
Persona Phone Leads
For strategic campaigns with multiple phone personas, add extra phone numbers in personaPhoneConfig. See Lead Phone Persona Management for the required campaign and phone-persona setup.