Skip to content
VoxaDocs
Navigation
Open console →

Calls

Batch calls

A batch calls every contact in a CSV or Excel file with one agent: you upload the list, check it, pick a start time and retry rules, and Voxa works through it inside the agent's calling hours, then gives you the results as a CSV.

How it works

  1. Upload the contact list (POST /api/v1/batches/uploads). Voxa reads it, finds the phone number column, and returns a preview: the first rows, the rows it will skip and why, and which of the agent’s {variables} the columns fill. Nothing is called yet; the file is kept for 24 hours.
  2. Create the batch from the upload (POST /api/v1/batches) with an agent, an optional caller number, a start time and retry rules.
  3. At the start time Voxa begins placing the calls. Each one is an ordinary outbound call: it waits in your workspace’s queue, uses a call slot, respects the agent’s calling hours and is billed per second.
  4. Calls that end as no-answer or busy (you choose which outcomes) are tried again after a delay, up to the number of retries you set.
  5. When every contact has a final outcome the batch is completed. Download the results with GET /api/v1/batches/{id}/results.

In the console: Batches → New batch.

The contact file

A .csv or .xlsx file. The first non-empty row holds the column names; every other row is one contact.

phone,name,due_date,clinic
+919812345678,Rohan,3 October,Andheri
98123 45679,Asha,4 October,Bandra
09812345680,Imran,4 October,Andheri
RuleDetails
Formats.csv (UTF-8, with or without a byte order mark; separated by , ; tab or `
SizeAt most 10 MB and 10,000 contacts per batch.
ColumnsAt most 50. A column without a name is called column_1, column_2, …; a repeated name gets _2.
Empty rowsSkipped.
CellsTrimmed, at most 500 characters. Excel numbers keep their digits (919812345678, not 9.19812E+11).

The phone number column

Voxa picks the column named phone, phone_number, contact_number, mobile, number, contact, to or similar (case, spaces and underscores don’t matter). If no name matches, it takes the first column that mostly holds phone numbers. Change it in the console’s preview, or send phone_column when you upload or create the batch.

Numbers are normalised to E.164:

In the fileCalled as
+919812345678, +91 98123-45678+919812345678
9812345678 (10 digits, starting 6 to 9)+919812345678
09812345678+919812345678
919812345678+919812345678
0044 20 7946 0958+442079460958
+1 (415) 555-0100+14155550100

Numbers from other countries need their + and country code. A row is skipped, with its reason in the preview, when the number is empty, isn’t a phone number, or repeats a number from an earlier row (duplicate of row 3).

Variables

Every other column becomes the contact’s user_data, exactly as for a single call (see User data). A column fills the agent’s {variable} with the same name, so for a prompt that says “Remind {name} about the visit on {due_date}”, name the columns name and due_date. Empty cells are left out, and the agent asks the caller for what it needs instead.

The preview lists the agent’s variables as covered (a column has that name) or missing. In the console, Sample CSV downloads a one-row file with the agent’s variables as columns.

Upload a file

POST /api/v1/batches/uploads (multipart, calls.place). Optional form fields: agent_id (to check the columns against its variables) and phone_column.

curl -X POST https://voxa.abhinavyadav.in/api/v1/batches/uploads \
  -H "X-API-Key: $VOXA_API_KEY" \
  -F "file=@contacts.csv" \
  -F "agent_id=$AGENT_ID"

201 Created:

{
  "upload_id": "5b0e3c1f9a2d4e6f8a1b2c3d4e5f6a7b",
  "filename": "contacts.csv",
  "size": 142,
  "expires_at": "2026-10-02T10:15:00+05:30",
  "columns": ["phone", "name", "due_date", "clinic"],
  "phone_column": "phone",
  "rows_total": 3,
  "valid_count": 3,
  "invalid_count": 0,
  "invalid": [],
  "preview": [
    {"row": 1, "values": {"phone": "+919812345678", "name": "Rohan", "due_date": "3 October", "clinic": "Andheri"}, "phone": "+919812345678", "error": ""}
  ],
  "variables": {"agent": ["name", "due_date"], "covered": ["name", "due_date"], "missing": [], "extra_columns": ["clinic"]},
  "limits": {"rows_max": 10000, "file_max_mb": 10}
}

row numbers count contacts: 1 is the first row under the header. preview holds the first 20 rows and invalid the first 200 skipped rows ({"row", "value", "reason"}); the counts cover the whole file.

To see the preview with another phone column or agent, call GET /api/v1/batches/uploads/{upload_id}?phone_column=mobile&agent_id=....

ErrorMeaning
413The file is over 10 MB or has more than 10,000 contacts.
415Not a .csv or .xlsx file, or the content doesn’t match the extension.
400Empty file, a header with no rows, or a file that can’t be read.

Create a batch

POST /api/v1/batches (calls.place).

curl -X POST https://voxa.abhinavyadav.in/api/v1/batches \
  -H "X-API-Key: $VOXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "upload_id": "5b0e3c1f9a2d4e6f8a1b2c3d4e5f6a7b",
    "agent_id": "'"$AGENT_ID"'",
    "name": "October reminders",
    "start_at": "2026-10-02T10:00:00",
    "timezone": "Asia/Kolkata",
    "retry": {"outcomes": ["no-answer", "busy"], "max_retries": 2, "delay_minutes": 60}
  }'
FieldTypeDefaultWhat it does
upload_idstringrequiredFrom the upload. It is used up by the batch.
agent_idstringrequiredThe agent that makes every call.
namestringthe file nameUp to 120 characters.
phone_columnstringdetectedThe column with the phone numbers.
from_numberstringthe agent’s phone_numberThe caller ID for every call.
start_atdatetimenowWhen to start. A time without an offset is read in timezone; a time in the past means now. At most 30 days ahead.
timezonestringAsia/KolkataIANA zone for start_at.
retry.outcomeslist["no-answer", "busy"]Which call outcomes are retried: any of no-answer, busy, failed.
retry.max_retriesinteger1Extra attempts per contact, 0 to 3.
retry.delay_minutesinteger30Wait after a call ends before trying that contact again, 1 to 1440.

201 Created returns the batch with status: "scheduled". It starts within a few seconds of start_at.

ErrorMeaning
400 no caller numberNeither from_number nor the agent’s phone_number is set.
400 the file has no valid phone numbersEvery row was skipped.
402 out of creditsThe batch would start now and the workspace has no credit. A batch scheduled for later can be created; if there is still no credit at its start time it is paused with status_reason: "out of credits".
404 upload not foundThe upload expired (24 hours), was already used, or belongs to another workspace.

Scheduling and calling hours

Calls are placed only inside the agent’s calling hours (call.call_start_hour to call.call_end_hour in call.timezone, 09:00 to 21:00 Asia/Kolkata by default; see Call tab). A batch that starts at 20:30 calls until 21:00, stops, and continues at 09:00 the next morning. Retries wait for the calling hours too.

Batch calls share your workspace’s concurrent call limit with every other call. A batch never fills the queue: it puts at most as many calls in the queue as your workspace has call slots, and adds more as calls end. Calls you place yourself while a batch runs are not stuck behind thousands of batch calls. See Concurrency and limits.

Retries

When a call ends, its contact gets a final outcome, or waits for a retry if all of these are true:

  • the call ended with one of retry.outcomes;
  • the contact has had fewer than 1 + retry.max_retries calls;
  • the batch is running or paused.

The next attempt is due retry.delay_minutes after the call ended, and is placed at the first moment after that when the agent’s calling hours are open and a slot is free. Due retries go before contacts not called yet.

Each attempt is a separate call. Every call of a batch carries:

FieldMeaning
batch_idThe batch.
batch_rowThe contact’s row (1 = first contact).
attempt1 for the first call, 2 for the first retry, and so on.

List a batch’s calls with GET /api/v1/calls?batch_id={id}. In webhooks, the call.queued event of a batch call has batch_id, batch_row and attempt in its data, and call.completed carries the whole call record with these fields, so you can tell batch calls apart. There are no separate batch events: poll GET /api/v1/batches/{id} for the batch’s progress.

A call the queue fails because the workspace ran out of credit does not count as an attempt: the contact goes back to the list and the batch pauses.

Statuses

Batch statusMeaning
scheduledWaiting for start_at.
runningPlacing calls.
pausedPaused by someone (status_reason: "paused by <name>") or because credits ran out ("out of credits"). No new calls; resume it to carry on.
completedEvery contact has a final outcome.
canceledCanceled; contacts not called were marked canceled.
failedThe batch could not go on (status_reason: "the agent was deleted").
Contact statusMeaning
pendingNot called yet.
callingA call is queued, ringing or in progress.
waiting_retryThe last call was retryable; next_attempt_at says when the next one is due.
completed, no-answer, busy, failedThe final outcome: the status of the contact’s last call.
canceledThe batch was canceled before this contact was reached, or its call was canceled.

The batch object

GET /api/v1/batches/{id} (calls.view):

{
  "id": "c4f1e2d3b4a5968778695a4b3c2d1e0f",
  "name": "October reminders",
  "agent_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "agent_name": "Reminder",
  "from_number": "+918035001234",
  "filename": "contacts.csv",
  "columns": ["phone", "name", "due_date", "clinic"],
  "phone_column": "phone",
  "status": "running",
  "status_reason": "",
  "scheduled_at": "2026-10-02T10:00:00+05:30",
  "timezone": "Asia/Kolkata",
  "retry": {"outcomes": ["no-answer", "busy"], "max_retries": 2, "delay_minutes": 60},
  "total": 1240,
  "invalid_rows": 12,
  "counts": {
    "pending": 900, "calling": 5, "waiting_retry": 41, "completed": 260,
    "no-answer": 20, "busy": 4, "failed": 10, "canceled": 0, "calls": 380
  },
  "done": 294,
  "progress": 0.2371,
  "created_by": "Asha Rao",
  "created_at": "2026-10-01T18:20:00+05:30",
  "updated_at": "2026-10-02T10:00:04+05:30",
  "started_at": "2026-10-02T10:00:04+05:30",
  "finished_at": null
}

counts has the number of contacts in each contact status, plus calls (calls placed so far, retries included). done is the number of contacts with a final outcome and progress is done / total.

Other endpoints:

RequestPermissionWhat it does
GET /api/v1/batches?status=running,paused&agent_id=...&limit=50&skip=0calls.viewBatches, newest first: {"items": [...], "total": n}.
GET /api/v1/batches/{id}/contacts?status=no-answer,busy&q=Rohan&limit=50&skip=0calls.viewContacts in file order: row, phone, phone_input (as written in the file), user_data, status, attempts, last_call_id, last_call_status, next_attempt_at. q searches the number and the first ten columns.
POST /api/v1/batches/{id}/pausecalls.placeStop placing calls. Calls still waiting in the queue are canceled and their contacts go back to the list (the attempt doesn’t count); calls already ringing finish.
POST /api/v1/batches/{id}/resumecalls.placeCarry on (402 without credit). A batch paused before it started goes back to scheduled.
POST /api/v1/batches/{id}/cancelcalls.placeCancel every contact not called yet and the calls still queued. Calls already ringing or in progress finish and record their outcome.
DELETE /api/v1/batches/{id}calls.placeDelete a completed, canceled or failed batch and its contact list (409 otherwise, or while a call is still in progress). Its calls stay in the call history.
GET /api/v1/batches/{id}/resultscalls.viewThe results CSV.
curl -X POST https://voxa.abhinavyadav.in/api/v1/batches/$BATCH_ID/pause -H "X-API-Key: $VOXA_API_KEY"

Results CSV

curl https://voxa.abhinavyadav.in/api/v1/batches/$BATCH_ID/results \
  -H "X-API-Key: $VOXA_API_KEY" -o results.csv

One row per contact, in file order. Rows skipped at upload are not included.

ColumnValue
rowThe contact’s row.
your columnsAs in your file (the phone column as you wrote it).
phone_e164The number Voxa called.
statusThe contact status.
attemptsCalls placed to this contact.
last_call_id, last_call_statusThe latest call and how it ended.
duration_seconds, cost_paise, summaryFrom the latest call.
extracted.<key>One column per field your agent’s analytics extract.

The file is UTF-8 with a byte order mark, so Excel opens it correctly. A cell from your data or the summary that starts with =, @, + or - (other than a number) is prefixed with ' so a spreadsheet never runs it as a formula.

Limits

LimitValue
File size10 MB
Contacts per batch10,000
Columns50
Retries per contact0 to 3
Delay between attempts1 to 1,440 minutes
Schedule ahead30 days
Upload kept24 hours

Calls also follow your workspace’s concurrent call limit, its credits, and the 24-hour queue expiry of outbound calls.

Esc