# VoiceML Reference

> VoiceML is the JSON instruction format for Edesy calls. Reference for all 11 verbs - say, play, gather, record, dial, stream, redirect, enqueue, reject, hangup, pause - with examples.

Source: https://edesy.in/docs/voice-api/calls/voiceml

VoiceML is the JSON format that tells an Edesy call what to do. It is an object with an `instructions` array; each instruction has a `verb` and that verb's fields, and the call runs them in order. Your answer URL returns VoiceML, and you can also send it inline as the `voiceml` field of `POST /v1/calls`. VoiceML is JSON, not TwiML XML.

```json
{
  "instructions": [
    { "verb": "say", "text": "Welcome to Acme." },
    { "verb": "gather", "text": "Press 1 to confirm your order, or 2 to cancel.", "num_digits": 1, "timeout": 8, "action": "https://example.com/edesy/menu" },
    { "verb": "say", "text": "We did not get your answer. Goodbye." },
    { "verb": "hangup" }
  ]
}
```

## Verbs at a glance

| Verb | What it does |
|---|---|
| [`say`](#say) | Speak text with text-to-speech |
| [`play`](#play) | Play an audio file from a URL |
| [`gather`](#gather) | Collect keypad digits |
| [`record`](#record) | Record a message from the caller |
| [`dial`](#dial) | Connect the call to another phone number |
| [`stream`](#stream) | Send the call audio to a WebSocket |
| [`redirect`](#redirect) | Continue with instructions from another URL |
| [`enqueue`](#enqueue) | Place the caller in a named queue (see the note) |
| [`reject`](#reject) | Decline an inbound call without answering |
| [`hangup`](#hangup) | End the call |
| [`pause`](#pause) | Wait in silence |

Rules that apply to every document:

- Instructions run top to bottom. When the last one finishes, the call ends.
- An instruction with an unknown `verb` is skipped. If nothing valid is left, the caller hears a short error message and the call ends.
- Numbers are JSON numbers (`"timeout": 8`), not strings.
- Host audio files and callback URLs on public `https://` addresses.

## say

Speaks `text` with text-to-speech.

```json
{ "verb": "say", "text": "आपका ऑर्डर कल पहुंच जाएगा।", "language": "hi-IN" }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes | What to say |
| `language` | string | no | BCP-47 code, for example `en-IN`, `hi-IN`, `ta-IN` |
| `voice` | string | no | Voice ID. Omit to use the platform default voice |

Characters spoken with `say` are metered. Premium voices cost Rs 0.03 per character on top of call minutes. See [Pricing and Limits](https://edesy.in/docs/voice-api/getting-started/pricing-and-limits).

## play

Plays an audio file.

```json
{ "verb": "play", "url": "https://cdn.example.com/welcome.mp3", "loop": 2 }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | Public URL of a WAV or MP3 file |
| `loop` | integer | no | How many times to play it. Default 1 |

`play` carries no TTS charge, so pre-recorded audio is the cheapest way to deliver a fixed message.

## gather

Speaks an optional prompt and collects keypad (DTMF) digits.

```json
{
  "verb": "gather",
  "text": "Please enter your 4-digit PIN, then press hash.",
  "num_digits": 4,
  "finish_on_key": "#",
  "timeout": 10,
  "action": "https://example.com/edesy/pin"
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | no | Prompt spoken while waiting for input |
| `num_digits` | integer | no | Stop after this many digits |
| `finish_on_key` | string | no | Key that ends input, for example `#` |
| `timeout` | integer | no | Seconds to wait for input |
| `action` | string | no | URL that receives the digits |

If the caller enters nothing before `timeout`, the call moves on to the next instruction, so put a fallback `say` after the `gather`.

`gather` collects keypad digits. For spoken answers, use [Conversation Relay](https://edesy.in/docs/voice-api/streaming/conversation-relay) (Edesy transcribes speech for you) or [Media Streams](https://edesy.in/docs/voice-api/streaming/media-streams) (you run your own speech recognition).

## record

Records the caller, for example a voicemail. The recording is stored and listed by `GET /v1/recordings`.

```json
{ "verb": "record", "max_length": 120, "play_beep": true, "finish_on_key": "#" }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `max_length` | integer | no | Maximum length in seconds |
| `play_beep` | boolean | no | Play a beep before recording starts |
| `finish_on_key` | string | no | Key that stops the recording |
| `action` | string | no | URL notified when recording ends |

If a recording cannot be captured, the caller hears a short message instead and the call continues. To record a whole call, turn on recording for the number or application instead. See [Recordings and Transcriptions](https://edesy.in/docs/voice-api/calls/recordings-and-transcriptions).

## dial

Connects the caller to another phone number.

```json
{ "verb": "dial", "number": "+9198XXXXXXXX", "caller_id": "+9180XXXXXXXX", "timeout": 30 }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `number` | string | yes | E.164 number to dial |
| `caller_id` | string | no | Caller ID shown to the dialled party. Use a number in your workspace |
| `timeout` | integer | no | Seconds to ring, up to 600 |
| `action` | string | no | URL notified when the dialled leg ends |

To send inbound calls to a SIP endpoint rather than a phone number, use the SIP target in [Inbound Routing](https://edesy.in/docs/voice-api/phone-numbers/inbound-routing).

## stream

Sends the call's audio to a WebSocket.

```json
{ "verb": "stream", "url": "wss://bot.example.com/ws" }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | `wss://` endpoint |

For a two-way voice bot, prefer `stream_url` on `POST /v1/calls` or the media-stream inbound target. Those set up a bidirectional L16 stream with your choice of 8 kHz or 16 kHz and record `call.stream.*` events. See [Media Streams](https://edesy.in/docs/voice-api/streaming/media-streams).

## redirect

Continues the call with instructions from another URL.

```json
{ "verb": "redirect", "url": "https://example.com/edesy/next-step" }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | URL to fetch next |

## enqueue

```json
{ "verb": "enqueue", "queue_name": "support" }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `queue_name` | string | yes | Queue name |

`enqueue` is part of the VoiceML schema, but the answer-URL runtime does not execute it today and skips it. To queue callers for live agents, use the Edesy contact center at [ccaas.edesy.in](https://ccaas.edesy.in), which has queues, routing strategies, hold music and an agent desktop.

## reject

Declines an inbound call without answering it, so neither side is billed for talk time.

```json
{ "verb": "reject", "status": 486, "reason": "Busy" }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `status` | integer | no | SIP status from 400 to 699. Default `603` (decline). Use `486` for busy |
| `reason` | string | no | Reason phrase sent with the status |

`reject` only works before the call is answered, so make it the first instruction your inbound webhook returns.

## hangup

Ends the call.

```json
{ "verb": "hangup" }
```

No fields.

## pause

Waits in silence.

```json
{ "verb": "pause", "length": 2 }
```

| Field | Type | Required | Description |
|---|---|---|---|
| `length` | number | no | Seconds of silence. Default 1 |

## Callback URLs (`action`, `redirect`)

The answer URL is the documented, supported way to control a call: Edesy fetches it once and runs the whole document it returns. The `action` URLs on `gather`, `record` and `dial`, and the `redirect` target, are called directly by the media layer. Their request and response format is different from the answer URL and is not covered in these docs yet.

For a keypad menu with several steps, build the flow in [IVR Studio](https://ivr.edesy.in) and route the number to it, or run the conversation from your own server with [Conversation Relay](https://edesy.in/docs/voice-api/streaming/conversation-relay) or [Media Streams](https://edesy.in/docs/voice-api/streaming/media-streams).

## Examples

**Delivery notice with a recorded jingle**

```json
{
  "instructions": [
    { "verb": "play", "url": "https://cdn.example.com/brand-jingle.mp3" },
    { "verb": "say", "text": "Your Acme parcel will be delivered today between 2 and 5 PM.", "language": "en-IN" },
    { "verb": "pause", "length": 1 },
    { "verb": "say", "text": "Thank you." },
    { "verb": "hangup" }
  ]
}
```

**After-hours voicemail on an inbound number**

```json
{
  "instructions": [
    { "verb": "say", "text": "Our office is closed. Please leave a message after the beep." },
    { "verb": "record", "max_length": 90, "play_beep": true },
    { "verb": "say", "text": "Thank you. We will call you back." },
    { "verb": "hangup" }
  ]
}
```

**Forward an inbound call to a mobile**

```json
{
  "instructions": [
    { "verb": "say", "text": "Connecting you to our team." },
    { "verb": "dial", "number": "+9198XXXXXXXX", "caller_id": "+9180XXXXXXXX", "timeout": 25 },
    { "verb": "say", "text": "Nobody is available right now. Please call again later." },
    { "verb": "hangup" }
  ]
}
```
