MCP is live.Set upAsk on Discord
OmniDimension
Bulk calls

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).

POST/calls/bulk_call/create
Body
14 fields
·

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
}'
Example response
{
  "status": "success",
  "message": "Bulk call created successfully",
  "id": 314,
  "is_scheduled": false,
  "is_dynamic": false,
  "current_status": "pending"
}

Authorization

BearerAuth
AuthorizationBearer <token>

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/json

Response 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_id is one of your numbers, from List phone numbers. The agent attached to that number runs the calls; pass bot_id to use a different agent, or when the number has none attached.
  • Any extra key on a contact row (customer_name here) reaches the agent as context for that one call, so it can greet the person by name or reference their account.
  • concurrent_call_limit is 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 toAddCovered in
Build the campaign first, start it latersave_as_draftDrafts
Spread calls across several numbersrotationRotation
Call only the contacts that matchcall_conditionsFiltering
Start at a set time, retry no-answersis_scheduled, retry_configScheduling and retries
Feed contacts in real time from a CRMis_dynamicDynamic 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.numbers do the dialing. phone_number_id becomes a standby, used only if every rotation number is paused. It is not added to the rotation for you, so list it in numbers too (as here) if you want it taking calls.
  • sequence is dialing order, lowest first. Any integers work; 1, 2 is the same as 10, 20.
  • strategy defaults to fixed_count: move to the next number every calls_per_number calls. cpr_threshold instead rotates when a number's health score drops below health_threshold, and both does whichever comes first. When every number is below the threshold, fallback decides: pause the campaign, or continue_best with 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"}
  ]
}
ContactplanbalanceOutcome
+15551110001pro240Called
+15551110002free900Skipped: wrong plan
+15551110003pro12Skipped: 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 saysWhyFix
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 anyoneSend 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 accountGet 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 numberMatch 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.numbersSend each number once
rotation needs a numbers array with at least one of your phone numbers.A rotation object arrived without numbersAdd the numbers, or drop the rotation object
Phone number ... was not found on your account.A rotation number is not yoursUse ids from List phone numbers
Retry limit must be between 1 and 10.retry_limit was 0 or over 10Omit it to disable retries; never send 0
auto_retry_schedule must be one of: immediately, next_day, scheduled_timeA schedule value outside the enumPick one of the three
Concurrent call limit must be greater than 0concurrent_call_limit was 0 or negativeSend at least 1, or omit it
scheduled_datetime property is required when is_scheduled is trueScheduled with no timeAdd scheduled_datetime and timezone
Scheduled datetime must be in the future.The time already passed in the given timezoneCheck the timezone; times are interpreted in timezone, not UTC
contact_list is required unless is_dynamic is trueA static campaign with nobody to callSend contacts, or make it dynamic
Row 3: ... with invalid_phone_numberA contact number failed validationNumbers 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 toCall
Progress counts for a dashboardBulk call live status
Per-contact outcomes, variables, recordingsBulk call results
Pause, resume, or reschedule the campaignBulk call actions
Speed up or slow downChange concurrency
See or steer the number rotationList rotation pool
Restrict dialing to business hoursSet calling hours
Re-queue contacts that did not connectRetry 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 ↗