Agents
Call tab
The Call tab controls how a conversation runs and ends: interruptions, silence handling, the "are you still there?" check, hanging up with a prompt, the maximum length, calling hours and recording.
Fields
All fields live under call in the agent object.
| Field | Type | Default | Allowed | Section |
|---|---|---|---|---|
record | boolean | true | Recording | |
allow_interruptions | boolean | true | Interruptions | |
interruption_min_words | integer | 2 | 1 to 10 | Interruptions |
check_user_online | boolean | false | User online check | |
user_online_message | string | Are you still there? | User online check | |
user_online_after_seconds | integer | 9 | 3 to 60 | User online check |
hangup_after_silence_seconds | integer | 15 | 5 to 120 | Hang up on silence |
hangup_on_prompt | boolean | false | Hang up using a prompt | |
hangup_prompt | string | see below | up to 4,000 characters | Hang up using a prompt |
hangup_interruptible | boolean | false | Hang up using a prompt | |
hangup_message | string | "" | {variables} allowed | Hang-up message |
max_duration_seconds | integer | 600 | 30 to 3600 | Maximum length |
call_start_hour | integer | 9 | 0 to 23 | Calling hours |
call_end_hour | integer | 21 | 1 to 24 | Calling hours |
timezone | string | Asia/Kolkata | an IANA time zone | Calling hours |
ambient_sound | string | "" | "", office, call_centre, cafe, street or upload:<id> | Ambient noise |
ambient_volume | integer | 30 | 0 to 100 | Ambient noise |
noise_cancellation | boolean | false | Noise cancellation | |
noise_cancellation_strength | integer | 50 | 0 to 100 | Noise cancellation |
voicemail_detection | boolean | false | Voicemail detection | |
voicemail_detection_seconds | integer | 5 | 2 to 10 | Voicemail detection |
voicemail_message | string | "" | up to 1,000 characters, {variables} allowed | Voicemail detection |
dtmf_input | boolean | false | Keypad input | |
dtmf_wait_seconds | number | 2.5 | 0.5 to 10 | Keypad input |
auto_reschedule | boolean | false | Auto reschedule | |
reschedule_max_days | integer | 7 | 1 to 30 | Auto reschedule |
transfer_enabled | boolean | false | needs at least one target | Transfer to a human |
transfer_targets | list | [] | up to 5 {name, number, description} | Transfer to a human |
transfer_message | string | Please hold, I'm connecting you to {target}. | up to 300 characters, {variables} and {target} allowed | Transfer to a human |
transfer_summary | boolean | true | Transfer to a human |
{
"call": {
"record": true,
"allow_interruptions": true,
"interruption_min_words": 2,
"check_user_online": true,
"user_online_message": "Hello, are you still there?",
"user_online_after_seconds": 8,
"hangup_after_silence_seconds": 20,
"hangup_on_prompt": true,
"hangup_prompt": "You are an AI assistant determining if a conversation is complete. ...",
"hangup_interruptible": false,
"hangup_message": "Thank you {name}, have a great day!",
"max_duration_seconds": 300,
"call_start_hour": 10,
"call_end_hour": 19,
"timezone": "Asia/Kolkata"
}
}
The console’s New agent template turns
check_user_onlineon. An agent created through the API without it has the check off.
Interruptions
With allow_interruptions on, the caller can talk over the agent. As soon as the caller has said at least interruption_min_words words while the agent is speaking or preparing a reply, Vaakyo:
- stops the reply and clears the audio not yet played;
- keeps only the part of the reply the caller heard, marked
"interrupted": truein the transcript; - emits an
interruptedevent; - answers what the caller says.
interruption_min_words stops coughs and short listening words (“haan”, “ok”, “hmm”) from cutting the agent off. Fewer words than that while the agent is speaking are kept in the transcript but not answered: the agent carries on. Raise it to 3 or 4 on noisy lines.
A stop word always interrupts, however short: “stop”, “wait”, “hold on”, “sorry”, “excuse me”, “ruko”, “ruk jao”, “band karo”, “ek minute”, “nahi” (also in Devanagari).
“The part the caller heard” is measured by when the audio actually played, not by what was sent: phone audio reaches Plivo faster than it plays, so a reply cut off halfway keeps only its first half, in the transcript and in what the agent remembers.
With allow_interruptions off, nothing the caller says stops the agent once it has started speaking. What they say meanwhile is kept and answered as soon as the agent finishes. If the caller adds to their turn while the agent is still preparing a reply (before any audio), that reply is dropped and the agent answers everything they said.
Once the agent has decided to hang up (with end_call, the hang-up prompt or on silence), nothing the caller says interrupts the goodbye. Those last words are kept in the transcript but not answered.
User online check
When the caller goes quiet, the agent can ask once whether they are still there.
| Field | What it does |
|---|---|
check_user_online | Turns the check on. |
user_online_message | What the agent says. If empty, the check is skipped. |
user_online_after_seconds | Seconds of silence before asking. Must be less than hangup_after_silence_seconds, or the check never runs. |
Silence is counted from the later of: the last time the caller spoke, or the end of the agent’s last audio. The question is asked once per stretch of silence; when the caller speaks again, the check resets. It emits a user.online_check event.
Hang up on silence
If neither side speaks for hangup_after_silence_seconds, the agent ends the call. If there is a hangup_message, the agent says it first. The call ends with hangup_by: "agent" and hangup_reason: "caller was silent".
The timer does not run while the agent is preparing or speaking a reply, or waiting for a tool.
Hang up using a prompt
In the console this is on the Agent tab, under the prompt.
With hangup_on_prompt on, after each reply the LLM writes, Vaakyo asks the agent’s LLM a separate short question: given the transcript so far and hangup_prompt, is the conversation complete? The answer is structured ({"complete": true} or false), so your prompt only has to describe when a call is over; it doesn’t need to ask for a “Yes” or “No”. When the answer is true, the agent says the hangup_message (if any) and hangs up. The call ends with hangup_reason: "hang-up prompt: conversation complete" and a hangup.prompt event.
- The check runs in the background; it does not delay the next reply.
- If the caller has started talking again by the time the answer comes back, the call goes on.
- The check uses the agent’s
llm.modelwith temperature 0, and its tokens count in the call’susage. - It does not run after scripted lines (welcome, “are you still there?”) or after a reply that already ended the call.
The default hangup_prompt is:
You are an AI assistant determining if a conversation is complete. A conversation is complete if:
1. The user explicitly says they want to stop (e.g., "That's all," "I'm done," "Goodbye," "thank you").
2. The user seems satisfied, and their goal appears to be achieved.
3. The user's goal appears achieved based on the conversation history, even without explicit confirmation.
If none of these apply, the conversation is not complete.
Write your own when your calls have a different end, for example “complete once the caller has confirmed or declined the appointment, or asked not to be called again”.
The decision is made by an LLM, so it can be wrong. Test it on real calls, tune the prompt, and keep
max_duration_secondsas a hard limit.
Letting the caller interrupt the goodbye
With hangup_interruptible off (the default), once the hang-up prompt decides the call is over, the caller can’t talk the goodbye away: the agent finishes the hangup_message and hangs up. Turn it on to let a caller who cuts in with something new (and at least interruption_min_words words) stop the goodbye; the hang-up is cancelled and the conversation goes on. This applies only to prompt hang-ups: silence and maximum-length hang-ups always end the call.
Hang-up message
hangup_message is said just before the agent hangs up on silence or by the hang-up prompt. Leave it empty to hang up without a word. {variables} are filled per call, like in the welcome message: "Thank you {name}, have a great day!".
It is not said when the LLM calls end_call (the LLM says its own goodbye in the same reply), when the call hits its maximum length or runs out of credit, or when the caller hangs up.
Maximum length
max_duration_seconds is the longest a call may last, counted from the moment the agent goes live. When it is reached, the call ends at once with hangup_reason: "maximum call length reached".
The limit is lowered for a call when your credits would run out sooner. In that case, the call ends with hangup_reason: "credits ran out" and an error event. See Limits and billing.
Calling hours
Outbound calls are dialled only when the current hour in timezone is at least call_start_hour and less than call_end_hour. With the defaults, calls go out from 9:00 to 20:59 India time.
- Calls placed outside the window wait in the queue until it opens (and expire after 24 hours).
ignore_call_window: trueon the place-call request skips the check for that call.- Inbound and browser calls ignore calling hours.
- The window can’t wrap past midnight. If
call_start_houris not less thancall_end_hour, outbound calls are never dialled. timezoneis an IANA name such asAsia/Kolkata,Asia/DubaiorEurope/London. It also sets the date and time the agent is told at the start of each call.
See Call flow.
Recording
With record on, Vaakyo saves a stereo WAV of the call on its server: the caller on the left channel, the agent on the right. The agent’s audio is placed where the caller actually heard it, and audio cut off by an interruption is removed.
- Phone calls are recorded at 8 kHz, browser calls at 16 kHz.
- Calls shorter than half a second are not saved.
- The call’s
recording_secondsgives the length. Download it withGET /api/v1/calls/{id}/recording(add?download=truefor a file name).
curl -o call.wav "https://api.vaakyo.com/api/v1/calls/$CALL_ID/recording?download=true" \
-H "X-API-Key: $VAAKYO_API_KEY"
A call without a recording returns 404 no recording for this call.
Ambient noise
ambient_sound plays a quiet background sound under the agent’s voice and through its pauses, so the call sounds like it comes from a real place. Choose a preset or upload your own clip; ambient_volume (0 to 100) sets how loud it is. "" (the default) or volume 0 plays nothing.
ambient_sound | Sound |
|---|---|
office | air conditioning, distant voices, occasional typing |
call_centre | many quiet voices and keyboards |
cafe | murmur of a busy room, cups and spoons |
street | traffic rumble, passing cars, wind |
upload:<id> | your workspace’s own clip |
- The sound loops seamlessly and keeps playing while the agent is silent. It never covers the agent: at volume 100 it sits about 10 to 15 dB under a voice, and it is turned down a little more while the agent talks.
- It adds no delay to the agent’s replies (it is mixed into the same audio), and it is in the call’s recording.
- It works on phone calls and browser test calls.
- The presets are generated by Vaakyo (no recordings of real places or people) and released under CC0.
Manage clips with the ambient endpoints (permission agents.view to list and preview, agents.edit to upload and delete):
| Request | Does |
|---|---|
GET /api/v1/ambient | the presets, then your uploads: [{"id": "office", "name": "Office", "seconds": 10, "kind": "preset"}, {"id": "upload:3f2a...", "name": "rain.wav", "seconds": 12.5, "kind": "upload"}] |
POST /api/v1/ambient | upload a clip (multipart field file): a PCM WAV file, at most 2 MB and 60 seconds. It is stored as 16 kHz mono. MP3 is not accepted yet. |
GET /api/v1/ambient/{id}/audio | the clip as a WAV, to preview it |
DELETE /api/v1/ambient/{id} | delete an upload; 409 while an agent uses it |
curl -X POST https://api.vaakyo.com/api/v1/ambient \
-H "X-API-Key: $VAAKYO_API_KEY" -F "file=@rain.wav"
An agent can only use its own workspace’s uploads (422 that ambient sound is not in this workspace otherwise).
Noise cancellation
With noise_cancellation on, the caller’s audio is cleaned before it reaches speech to text: steady background noise (a fan, traffic hum, line hiss, a busy room) is turned down, so the transcriber hears the caller more clearly and background noise is less likely to be taken for speech or an interruption. noise_cancellation_strength (0 to 100) sets how hard: higher removes more noise but can make a quiet voice sound thinner. 50 suits most calls; 0 does nothing.
- It learns the noise from the pauses in the caller’s speech (the quietest sound over the last 2 seconds), so it adapts within a couple of seconds when the noise changes. Sudden sounds (a door, a dog) are not removed.
- It adds 10 ms of delay and very little work: about 0.1 to 0.2 ms per 20 ms of audio on one CPU core. On a test signal it lowers steady noise by about 10 dB at strength 70 while the voice stays within 0.3 dB.
- The recording keeps the caller’s original audio.
- The transcribers are already trained on noisy phone audio; noise cancellation helps most on very noisy lines.
Voicemail detection
With voicemail_detection on, outbound phone calls ask Plivo to detect an answering machine. The call connects at once and the agent starts as usual; Plivo listens for voicemail_detection_seconds and reports what answered.
- A person: nothing changes. The call’s
answered_byishuman. - A machine:
answered_byismachineand acall.voicemailevent is added. With avoicemail_message, the agent stops what it was saying, speaks the message (its{variables}filled like the welcome message) and hangs up; without one it hangs up at once. Either way the call endscompletedwithhangup_reasonvoicemail. - Unsure:
answered_byisunknownand the call goes on.
{
"call": {
"voicemail_detection": true,
"voicemail_detection_seconds": 5,
"voicemail_message": "Hi {name}, this is Asha from Acme Clinics about your appointment. Please call us back on 080 4567 8900."
}
}
- Plivo reports a machine as soon as it hears one, often during the greeting, so the start of the message can overlap the greeting.
- Detection runs only on outbound phone calls; inbound and browser calls ignore it. Plivo may charge for it.
- In a batch, add
voicemailtoretry.outcomesto call again contacts whose machine answered.
Keypad input
With dtmf_input on, keys the caller presses on their phone reach the agent as if the caller had said them: [keypad: 1 2 3 #]. Keys pressed less than dtmf_wait_seconds apart arrive as one message; # sends what was typed at once. Pressing a key while the agent is talking interrupts it, however short the entry (with allow_interruptions off it is answered once the agent finishes). The agent’s prompt is told the format, so you can ask for “your 6-digit booking number, then press hash”.
- Works on phone calls, and in the browser test call (its keypad).
- Each message is a
call.dtmfevent with thedigitspressed. - With
dtmf_inputoff, keys are ignored.
Auto reschedule
With auto_reschedule on, a caller who asks to be called back later (“I’m driving, call me tomorrow at 5”) gets a new call booked for that time.
- In the call. The agent has a built-in
reschedule_calltool and its prompt is told the current local time, the timezone and the allowed window. It passes the time as a local date-time (2026-10-04T17:00, in the agent’stimezone). The time must be at least 5 minutes ahead, at mostreschedule_max_daysdays ahead, and inside the calling hours; otherwise the tool tells the agent what is allowed and the agent asks for another time. Once it is booked, the agent confirms the day and time, says goodbye and the call ends (hangup_reason: "call rescheduled"), even if the agent forgets to callend_call. - After the call. If the agent did not book one, a short check of the transcript (one request to the agent’s LLM, only with
auto_rescheduleon and only for phone calls that connected) asks whether the caller asked for a call back at a specific time; if so, and the time follows the same rules, it is booked.
A call books at most one call back: the tool wins, and the after-call check never adds a second.
The call back is an ordinary queued outbound call with the same agent, the same numbers (from the agent’s number to the customer’s), the same user_data and a scheduled_for time. It waits in the queue, without taking a call slot, until that time, then is dialled like any queued call (credits and free slots are checked then). The two calls link to each other:
| Field | On | Meaning |
|---|---|---|
rescheduled_to, rescheduled_for | the call where it was asked | The booked call’s id, and when it will ring. |
scheduled_for, rescheduled_from | the booked call | When it rings, and the call where it was asked. |
- Cancel a booked call like any queued call:
POST /api/v1/calls/{call_id}/hangup(or Cancel in the console). - Each booking adds a
call.rescheduledevent to the original call, withsourcetoolorpost_call. - In a batch, the call back belongs to the same batch and contact (the next attempt). The contact is
rescheduleduntil the call back ends, and that call’s outcome is the contact’s outcome; it does not hold one of the batch’s concurrent calls meanwhile. Pausing or canceling the batch cancels its booked calls (a paused batch’s contacts go back to be called again). - In a browser test call the agent can use the tool, but nothing is booked.
Transfer to a human
With transfer_enabled on, the agent can hand a phone call to a person: a support desk, a sales line, a manager. You list up to five transfer_targets, each with a name, a number in E.164 (+919876543210) and a description of when to use it.
{
"call": {
"transfer_enabled": true,
"transfer_targets": [
{"name": "Support desk", "number": "+919876543210", "description": "billing questions and refunds"},
{"name": "Sales", "number": "+918045671234", "description": "new orders and pricing"}
],
"transfer_message": "Please hold {name}, I'm connecting you to {target}.",
"transfer_summary": true
}
}
How it works:
- The agent has a built-in
transfer_calltool (arguments:target, one of your target names, andreason, one sentence) and its prompt lists each target with its description. It transfers when the caller needs what a target handles, or asks for a person. - The agent says
transfer_message({target}is the target’s name;{variables}are filled as in the welcome message), then the call is connected to the target’s number. The person sees the agent’s phone number as the caller id. The caller hears ringing. - With
transfer_summaryon, the person who answers first hears one short sentence, for example “Transferred call from Front desk: a caller on 9 1 9 8 … Reason: Wants a refund on order 42.”, then is connected. It is built from the call and the agent’sreason, without an extra LLM request. - The agent’s part of the call ends:
hangup_by: "agent",hangup_reason: "transferred to Support desk", and the call getstransferred_to({name, number}),transferred_atandtransfer_reason. The caller and the person keep talking. transfer_statussays how it went:connecting, thenanswered,no-answer(rang for 30 seconds),busy,failedorcanceled(the caller hung up while it rang). When no one answers, the caller hears “Sorry, no one is available to take your call right now. We’ll get back to you soon. Goodbye.” and the call ends.transfer_secondsis how long the person talked.
Billing: the agent’s part is charged as usual when it ends. The minutes with the person are charged when that leg ends, at your phone-minute (telephony) rate, with no platform fee: the call’s cost_paise grows by transfer_charged_paise, and cost_breakdown.lines gains a telephony line with model: "transfer". The connected leg is capped at what your credits cover at that rate when it starts (at most 4 hours).
- Phone calls only. Browser test calls don’t offer the tool at all, so trying the agent in the console never transfers; place a phone call to test it.
- Each transfer adds a
call.transferredevent (name,number,reason), then acall.transfer_statusevent (status). Webhook endpoints can subscribe to both.call.completedis sent when the agent’s part ends, so its record showstransfer_status: "connecting"; usecall.transfer_statusto learn whether the person answered. - In a batch, a transferred call counts as
completedwithoutcome: "transferred", and is never retried. - Turning transfers on with no targets is refused (
422), as is a number not in E.164, more than five targets, or two targets with the same name.transfer_callcan’t be the name of your own tool. - If the carrier refuses the transfer, the agent says it couldn’t connect the caller and the conversation goes on (an
errorevent says why).