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
- 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. - Create the batch from the upload (
POST /api/v1/batches) with an agent, an optional caller number, a start time and retry rules. - 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.
- Calls that end as
no-answerorbusy(you choose which outcomes) are tried again after a delay, up to the number of retries you set. - When every contact has a final outcome the batch is
completed. Download the results withGET /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
| Rule | Details |
|---|---|
| Formats | .csv (UTF-8, with or without a byte order mark; separated by , ; tab or ` |
| Size | At most 10 MB and 10,000 contacts per batch. |
| Columns | At most 50. A column without a name is called column_1, column_2, …; a repeated name gets _2. |
| Empty rows | Skipped. |
| Cells | Trimmed, 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 file | Called 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=....
| Error | Meaning |
|---|---|
413 | The file is over 10 MB or has more than 10,000 contacts. |
415 | Not a .csv or .xlsx file, or the content doesn’t match the extension. |
400 | Empty 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}
}'
| Field | Type | Default | What it does |
|---|---|---|---|
upload_id | string | required | From the upload. It is used up by the batch. |
agent_id | string | required | The agent that makes every call. |
name | string | the file name | Up to 120 characters. |
phone_column | string | detected | The column with the phone numbers. |
from_number | string | the agent’s phone_number | The caller ID for every call. |
start_at | datetime | now | When to start. A time without an offset is read in timezone; a time in the past means now. At most 30 days ahead. |
timezone | string | Asia/Kolkata | IANA zone for start_at. |
retry.outcomes | list | ["no-answer", "busy"] | Which call outcomes are retried: any of no-answer, busy, failed. |
retry.max_retries | integer | 1 | Extra attempts per contact, 0 to 3. |
retry.delay_minutes | integer | 30 | Wait 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.
| Error | Meaning |
|---|---|
400 no caller number | Neither from_number nor the agent’s phone_number is set. |
400 the file has no valid phone numbers | Every row was skipped. |
402 out of credits | The 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 found | The 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_retriescalls; - the batch is
runningorpaused.
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:
| Field | Meaning |
|---|---|
batch_id | The batch. |
batch_row | The contact’s row (1 = first contact). |
attempt | 1 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 status | Meaning |
|---|---|
scheduled | Waiting for start_at. |
running | Placing calls. |
paused | Paused by someone (status_reason: "paused by <name>") or because credits ran out ("out of credits"). No new calls; resume it to carry on. |
completed | Every contact has a final outcome. |
canceled | Canceled; contacts not called were marked canceled. |
failed | The batch could not go on (status_reason: "the agent was deleted"). |
| Contact status | Meaning |
|---|---|
pending | Not called yet. |
calling | A call is queued, ringing or in progress. |
waiting_retry | The last call was retryable; next_attempt_at says when the next one is due. |
completed, no-answer, busy, failed | The final outcome: the status of the contact’s last call. |
canceled | The 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:
| Request | Permission | What it does |
|---|---|---|
GET /api/v1/batches?status=running,paused&agent_id=...&limit=50&skip=0 | calls.view | Batches, newest first: {"items": [...], "total": n}. |
GET /api/v1/batches/{id}/contacts?status=no-answer,busy&q=Rohan&limit=50&skip=0 | calls.view | Contacts 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}/pause | calls.place | Stop 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}/resume | calls.place | Carry on (402 without credit). A batch paused before it started goes back to scheduled. |
POST /api/v1/batches/{id}/cancel | calls.place | Cancel 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.place | Delete 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}/results | calls.view | The 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.
| Column | Value |
|---|---|
row | The contact’s row. |
| your columns | As in your file (the phone column as you wrote it). |
phone_e164 | The number Voxa called. |
status | The contact status. |
attempts | Calls placed to this contact. |
last_call_id, last_call_status | The latest call and how it ended. |
duration_seconds, cost_paise, summary | From 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
| Limit | Value |
|---|---|
| File size | 10 MB |
| Contacts per batch | 10,000 |
| Columns | 50 |
| Retries per contact | 0 to 3 |
| Delay between attempts | 1 to 1,440 minutes |
| Schedule ahead | 30 days |
| Upload kept | 24 hours |
Calls also follow your workspace’s concurrent call limit, its credits, and the 24-hour queue expiry of outbound calls.