Calls
Campaigns (batch calls)
A campaign calls every contact in a CSV or Excel file with one agent from one phone number: you upload the list, check it, pick a start time, a concurrency cap and retry rules, and Vaakyo works through it inside the calling hours, then gives you the results as a CSV.
The console calls them campaigns; the API calls them batches (/api/v1/batches). They are the same thing.
How it works
- Upload the contact list (
POST /api/v1/batches/uploads). Vaakyo 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, a caller number, a start time, retry rules and, optionally, a concurrency cap, calling hours and a webhook URL. - At the start time Vaakyo 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: Campaigns → New campaign, in three steps:
- Campaign details: the name, the phone number to call from (one of your workspace’s numbers), the agent that makes the calls, the max concurrency (how many of this campaign’s calls run at the same time, up to your workspace’s limit) and, under Webhook settings (optional), a URL for this campaign’s call events.
- Contacts: upload the file, check the preview and the skipped rows, pick the phone number column, and choose which columns are passed to the agent as call variables.
- Schedule: start now or at a date and time (in a time zone you pick), the calling hours (the agent’s, or narrower hours for this campaign) and the retry rules.
Avoid being flagged as spam
Carriers and phone apps flag numbers that place many short or unanswered calls. To keep your number’s reputation:
- call during sensible local hours (for example 10:00 to 19:00), never late at night;
- call from a local number your contacts recognise, from their country or region;
- don’t over-dial: keep the concurrency modest, use few retries with a long delay between them, and remove numbers that never answer.
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
Vaakyo 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), unless you pass variable_columns when you create the batch: then only those columns are sent with the calls (the others stay in the results CSV). 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://api.vaakyo.com/api/v1/batches/uploads \
-H "X-API-Key: $VAAKYO_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://api.vaakyo.com/api/v1/batches \
-H "X-API-Key: $VAAKYO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"upload_id": "5b0e3c1f9a2d4e6f8a1b2c3d4e5f6a7b",
"agent_id": "'"$AGENT_ID"'",
"name": "October reminders",
"from_number": "+918035001234",
"start_at": "2026-10-02T10:00:00",
"timezone": "Asia/Kolkata",
"max_concurrency": 3,
"call_window": {"start_hour": 10, "end_hour": 19},
"webhook_url": "https://example.com/vaakyo/campaign-events",
"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. Use one of your workspace’s numbers. |
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 and call_window. |
max_concurrency | integer | the workspace’s limit | The most calls of this batch queued, ringing or in progress at the same time, 1 up to your workspace’s concurrent call limit. |
call_window | object | none | {"start_hour": 10, "end_hour": 19}: hours of the day (in timezone) when calls are placed, on top of the agent’s calling hours. start_hour 0 to 23, end_hour 1 to 24, end after start. |
webhook_url | string | none | Receives the lifecycle events of this batch’s calls. https on the public internet. See Campaign webhook. |
variable_columns | list | every column | The columns passed to the agent as user_data. Empty or missing: every column except the phone number. |
retry.outcomes | list | ["no-answer", "busy"] | Which call outcomes are retried: any of no-answer, busy, failed, voicemail (an answering machine picked up; needs the agent’s voicemail detection). |
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 max_concurrency can be at most the workspace's concurrency limit (5) | Ask for a higher limit from the account menu (Concurrent calls → Request more), or lower max_concurrency. |
400 no data column named ... | A variable_columns entry is not a column of the file (or is the phone column). |
422 | An invalid field, such as an http webhook URL or a call_window that ends before it starts. |
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.
A batch with a call_window is narrower still: contacts are released only while both the agent’s hours and the window (read in the batch’s timezone) are open. A batch with "call_window": {"start_hour": 10, "end_hour": 19} on an agent open 09:00 to 21:00 calls from 10:00 to 19:00.
Concurrency
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 it may run at once, 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.
max_concurrency caps one batch below the workspace’s limit: with "max_concurrency": 3, at most 3 of its calls are queued, ringing or in progress at any moment, whatever the workspace’s limit, which leaves the other slots for your other calls and campaigns. A batch never goes above the workspace’s limit, even when that limit is lowered after the batch was created. Without max_concurrency, the batch may use every slot.
Campaign webhook
A batch’s webhook_url receives the lifecycle events of the batch’s calls: call.queued, call.placed, call.started, call.ended and call.completed. Timeline events such as user.turn never go there; subscribe a workspace endpoint to them instead.
The requests are the same as every other webhook (see The request), signed with the workspace’s default secret, retried, and logged in the delivery log with endpoint_id: "campaign". An event also still goes to your workspace endpoints and to the agent’s webhook_url, so a server that already receives those does not need a campaign URL. Every payload’s call_id identifies the call, and call.queued carries batch_id, batch_row and attempt in its data.
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 (including the batch’s own webhook URL), 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. |
rescheduled | The caller asked to be called back; next_attempt_at says when (auto reschedule). |
completed, no-answer, busy, failed | The final outcome: the status of the contact’s last call. A contact also has outcome: the same, or voicemail or transferred for a completed call (a transferred call is never retried). |
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",
"variable_columns": null,
"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},
"max_concurrency": 3,
"call_window": {"start_hour": 10, "end_hour": 19},
"webhook_url": "https://example.com/vaakyo/campaign-events",
"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://api.vaakyo.com/api/v1/batches/$BATCH_ID/pause -H "X-API-Key: $VAAKYO_API_KEY"
Results CSV
curl https://api.vaakyo.com/api/v1/batches/$BATCH_ID/results \
-H "X-API-Key: $VAAKYO_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 Vaakyo 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. |
outcome | How the latest call went: its status, or voicemail (an answering machine) or transferred (the agent handed the caller to a person) for a completed 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 |
| Max concurrency | 1 to your workspace’s concurrent call limit |
| 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.