Create bulk call
Create a new bulk-call campaign. Only name, phone_number_id and a contact_list are needed to dial a list now; every other field adds one behaviour on top (drafts, rotation, filtering, scheduling, retries, dynamic feeding).
Name of the bulk call campaign.
The number this campaign calls from. With a `rotation`, the rotation numbers dial instead and this one is the standby.
Agent to run the campaign. Defaults to the agent attached to `phone_number_id`; required when the number has none.
Store the campaign without dialing; start it later with the start endpoint. See Drafts in the guide below.
curl -X POST 'https://backend.omnidim.io/api/v1/calls/bulk_call/create' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "name": "Customer follow-ups", "phone_number_id": "177", "bot_id": 512, "contact_list": [ { "phone_number": "+15551234567", "customer_name": "John Doe", "plan": "pro" }, { "phone_number": "+15559876543", "customer_name": "Jane Smith", "plan": "trial" } ], "call_conditions": [ { "column": "plan", "operator": "equals", "value": "pro" } ], "rotation": { "numbers": [ { "phone_number_id": 177, "sequence": 10 }, { "phone_number_id": 178, "sequence": 20 } ], "strategy": "fixed_count", "calls_per_number": 50 }, "is_scheduled": true, "scheduled_datetime": "2026-12-25 10:00:00", "timezone": "America/New_York", "concurrent_call_limit": 3, "retry_config": { "auto_retry": true, "auto_retry_schedule": "next_day", "retry_limit": 2 }, "enabled_reschedule_call": true }'
{ "status": "success", "message": "Bulk call created successfully", "id": 314, "is_scheduled": false, "is_dynamic": false, "current_status": "pending" }
Authorization
BearerAuth Bearer token authentication. Obtain your API key from the OmniDimension dashboard.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response body
application/jsonResponse Body
application/json
Your first campaign
The playground above opens on a request that shows most of what the endpoint accepts. You need far less: this is the smallest request that places calls, and everything else adds one behaviour on top, covered one at a time below.
{
"name": "Customer follow-ups",
"phone_number_id": "177",
"contact_list": [
{"phone_number": "+15551234567", "customer_name": "John Doe"},
{"phone_number": "+15559876543", "customer_name": "Jane Smith"}
],
"concurrent_call_limit": 3
}Three things to know about this request:
phone_number_idis one of your numbers, from List phone numbers. The agent attached to that number runs the calls; passbot_idto use a different agent, or when the number has none attached.- Any extra key on a contact row (
customer_namehere) reaches the agent as context for that one call, so it can greet the person by name or reference their account. concurrent_call_limitis how many calls dial at once, capped by your account's concurrency.
The campaign starts dialing the moment this returns:
{
"status": "success",
"message": "Bulk call created successfully and started immediately",
"id": 314,
"is_scheduled": false,
"is_dynamic": false,
"current_status": "in_progress",
"filtering_stats": {
"total_contacts": 2,
"filtered_contacts": 2,
"skipped_contacts": 0,
"filtered_percentage": 100
}
}id is the handle every other bulk-call endpoint takes. current_status tells
you what the campaign is doing: in_progress here, draft if you saved a
draft, scheduled if you scheduled it, waiting for a dynamic campaign with
no contacts yet. filtering_stats accounts for every contact you sent; with no
call_conditions it simply confirms all of them are queued.
From here, watch progress with Bulk call live status and read per-contact outcomes with Bulk call results.
Add one behaviour at a time
| You want to | Add | Covered in |
|---|---|---|
| Build the campaign first, start it later | save_as_draft | Drafts |
| Spread calls across several numbers | rotation | Rotation |
| Call only the contacts that match | call_conditions | Filtering |
| Start at a set time, retry no-answers | is_scheduled, retry_config | Scheduling and retries |
| Feed contacts in real time from a CRM | is_dynamic | Dynamic campaigns |
Drafts: build it first, start it later
A campaign created with save_as_draft: true is stored without dialing, so you
can assemble it across several requests:
{"name": "August sweep", "phone_number_id": "177", "save_as_draft": true}Then add contacts in batches of up to 1000 with Add contacts in bulk, set concurrency or calling hours, and fire it with Start a draft campaign once everything is in place. This is the shape to use when your contact list comes from somewhere else in pieces.
Rotation: spread calls across numbers
One number dialing a whole campaign collects spam reports and stops being answered. A rotation spreads the load and moves off a number before it burns:
{
"name": "August sweep",
"phone_number_id": "177",
"rotation": {
"numbers": [
{"phone_number_id": 177, "sequence": 1},
{"phone_number_id": 178, "sequence": 2}
],
"strategy": "fixed_count",
"calls_per_number": 50
}
}How the pieces fit:
- The numbers in
rotation.numbersdo the dialing.phone_number_idbecomes a standby, used only if every rotation number is paused. It is not added to the rotation for you, so list it innumberstoo (as here) if you want it taking calls. sequenceis dialing order, lowest first. Any integers work;1, 2is the same as10, 20.strategydefaults tofixed_count: move to the next number everycalls_per_numbercalls.cpr_thresholdinstead rotates when a number's health score drops belowhealth_threshold, andbothdoes whichever comes first. When every number is below the threshold,fallbackdecides:pausethe campaign, orcontinue_bestwith the healthiest number.- Agents follow the campaign. A rotation number with no agent attached gets this campaign's agent automatically. One attached to a different agent is refused by name, because it would dial while still belonging elsewhere.
While it runs, List rotation pool shows which number is dialing and how far into its cycle it is, and Pause or resume a pool number takes a number out of rotation without losing its history.
Filtering: call only part of your list
Send a whole export and let the campaign decide who qualifies. Each condition tests one key on your own contact rows, and a contact must pass all of them:
{
"name": "Renewals",
"phone_number_id": "177",
"call_conditions": [
{"column": "plan", "operator": "equals", "value": "pro"},
{"column": "balance", "operator": "greater_than", "value": "100"}
],
"contact_list": [
{"phone_number": "+15551110001", "plan": "pro", "balance": "240"},
{"phone_number": "+15551110002", "plan": "free", "balance": "900"},
{"phone_number": "+15551110003", "plan": "pro", "balance": "12"}
]
}| Contact | plan | balance | Outcome |
|---|---|---|---|
| +15551110001 | pro | 240 | Called |
| +15551110002 | free | 900 | Skipped: wrong plan |
| +15551110003 | pro | 12 | Skipped: balance too low |
Contacts that fail are kept with status Skipped, not dropped, so
Bulk call results still
shows them. Check filtering_stats in the create response before the campaign
gets far: if you meant to call most of the list and filtered_percentage comes
back tiny, a column name is misspelled.
The operators are equals, not_equals, contains (case-insensitive),
greater_than and less_than. value is always a string, "100" not 100.
A row whose value is not a number fails a numeric comparison rather than
erroring, so a stray "balance": "n/a" skips that one contact, not the
request.
One rule tightens later: contacts added to a conditioned campaign through
Add contacts in bulk are
rejected, not skipped, when their custom_variables are missing a condition's
column, since a missing column cannot be judged either way.
Scheduling and retries
{
"name": "Monday reminders",
"phone_number_id": "177",
"contact_list": [{"phone_number": "+15551234567"}],
"is_scheduled": true,
"scheduled_datetime": "2026-09-01 09:00:00",
"timezone": "Asia/Kolkata",
"retry_config": {
"auto_retry": true,
"auto_retry_schedule": "next_day",
"retry_limit": 2
}
}The datetime is interpreted in timezone and must be in the future. Retries
re-dial contacts that did not connect: immediately, next_day, or at a
scheduled_time offset by retry_schedule_days and retry_schedule_hours.
retry_limit must be between 1 and 10; to run without retries, leave
auto_retry false and omit it rather than sending 0.
A campaign that already ran can also be retried on demand with
Retry contacts, no
retry_config needed up front.
Dynamic campaigns: feed contacts in live
Set is_dynamic: true and the campaign stays alive accepting contacts in real
time instead of completing when its queue drains:
{"name": "Inbound leads", "phone_number_id": "177", "is_dynamic": true}contact_list is optional here. Feed it from a CRM, form, or automation with
Add contact one at a time,
or Add contacts in bulk
in batches; each contact is queued on arrival and dialed within the campaign's
operating hours.
When create is refused
The exact messages the endpoint returns, and what each one means:
| The API says | Why | Fix |
|---|---|---|
This phone number has no agent attached. Pass bot_id in the request, or attach an agent to the number first. | The number cannot answer as anyone | Send bot_id, or attach an agent to the number |
That agent does not exist on your account. Check bot_id. | bot_id is wrong or belongs to another account | Get the id from List agents |
... is attached to a different agent. | Your bot_id (or the campaign's agent) contradicts the agent already on that number | Match bot_id to the number's agent, re-attach the number, or leave it out |
... is listed more than once. Each number belongs in the pool once. | The same phone_number_id appears twice in rotation.numbers | Send each number once |
rotation needs a numbers array with at least one of your phone numbers. | A rotation object arrived without numbers | Add the numbers, or drop the rotation object |
Phone number ... was not found on your account. | A rotation number is not yours | Use ids from List phone numbers |
Retry limit must be between 1 and 10. | retry_limit was 0 or over 10 | Omit it to disable retries; never send 0 |
auto_retry_schedule must be one of: immediately, next_day, scheduled_time | A schedule value outside the enum | Pick one of the three |
Concurrent call limit must be greater than 0 | concurrent_call_limit was 0 or negative | Send at least 1, or omit it |
scheduled_datetime property is required when is_scheduled is true | Scheduled with no time | Add scheduled_datetime and timezone |
Scheduled datetime must be in the future. | The time already passed in the given timezone | Check the timezone; times are interpreted in timezone, not UTC |
contact_list is required unless is_dynamic is true | A static campaign with nobody to call | Send contacts, or make it dynamic |
Row 3: ... with invalid_phone_number | A contact number failed validation | Numbers are E.164, like +15551234567; the row index tells you which |
Refusals happen before anything is written, so a refused request never leaves a half-built campaign behind.
Operating a running campaign
| You want to | Call |
|---|---|
| Progress counts for a dashboard | Bulk call live status |
| Per-contact outcomes, variables, recordings | Bulk call results |
| Pause, resume, or reschedule the campaign | Bulk call actions |
| Speed up or slow down | Change concurrency |
| See or steer the number rotation | List rotation pool |
| Restrict dialing to business hours | Set calling hours |
| Re-queue contacts that did not connect | Retry contacts |
Runnable code
Runnable code: the whole lifecycle on this page as a Python and TypeScript CLI, printing each request before it sends.Open on GitHub ↗Fetch bulk calls
List bulk-call campaigns with pagination and optional status filter.
Add contact to dynamic campaign
Push a single contact into a dynamic bulk-call campaign in real time. Dynamic campaigns are created from the dashboard (Bulk Call > Create New Campaign > Dynamic Campaign) and stay alive waiting for contacts, so this webhook is how you feed them from a CRM, form, or automation platform. The contact is queued immediately, and the campaign starts calling it as soon as it is within operating hours.
