# Vapi (voice agent)

Source: https://docs.vieneu.io/docs/cloud-api/vapi

[Vapi](https://vapi.ai) builds voice agents that answer and place phone calls. It
speaks through a TTS provider of your choosing — and its `custom-voice` provider
lets that be any HTTPS endpoint. VieNeu implements exactly what it expects, so
your agent can answer in Vietnamese.

This matters because Vietnamese is essentially absent from the realtime TTS
vendors Vapi ships with.

## Configure the assistant

```json
{
  "voice": {
    "provider": "custom-voice",
    "server": {
      "url": "https://api.vieneu.io/api/v1/vapi/speech?voiceId=Ngọc%20Lan",
      "secret": "vn_sk_your_key_here",
      "timeoutSeconds": 30
    }
  }
}
```

That is the whole integration. Two details are doing the work:

**`secret` is your VieNeu API key.** Vapi sends it as the `X-VAPI-SECRET` header,
and its config has no field for an `Authorization` header — so this is where the
key goes. It is the same key, checked the same way, and it is billed to the same
account.

**The voice goes in the URL.** Vapi's request payload has no voice field, so pass
`?voiceId=` (URL-encoded). List the options with `GET /v1/audio/voices`. Add
`&engine=v4` for the premium engine if your plan includes it. Omit `voiceId`
entirely and you get the engine's default voice.

## What happens on each utterance

Vapi POSTs:

```json
{
  "message": {
    "type": "voice-request",
    "text": "Xin chào, tôi có thể giúp gì cho bạn?",
    "sampleRate": 24000
  }
}
```

VieNeu answers `200` with `Content-Type: application/octet-stream` and raw mono
16-bit little-endian PCM at exactly that sample rate, streamed as it is
generated rather than buffered — so the agent starts speaking sooner.

Vapi asks for 8000, 16000, 22050 or 24000 Hz depending on the transport. All four
are supported, and the response is always at the rate requested: raw PCM carries
no rate of its own, so anything else would come out at the wrong pitch.

## Billing and behaviour

Billed per submitted character, like the rest of `/v1`. Audio that gets cut short
is refunded automatically.

**AI refinement never runs on this endpoint**, regardless of your account
settings. An agent's job is to answer quickly, and an extra model round-trip in
front of every utterance is the opposite of that. Deterministic text preparation
still runs, so Vietnamese is pronounced correctly. If your agent's text contains
things you want read a particular way — currency, dates, product codes — normalize
it in your prompt, where you can see the result.

## Latency, honestly

**On the standard plans**, first audio leaves in roughly 1–2 seconds. That works
for an agent that answers a question, reads a menu, confirms a booking. It is
**slower than the 200–300 ms** the fastest English-only vendors reach, and it
will be noticeable as a beat before the agent speaks.

An Enterprise deal runs on a dedicated node where the latency ceiling is agreed
per contract — down to ≤ 250 ms — which is the version of this you want if the
beat is a dealbreaker. See
[Latency, honestly](./streaming#latency-honestly).

Two things worth doing: keep `timeoutSeconds` at 30 or above, and keep utterances
short — a paragraph costs more waiting than three sentences.

**Utterances are capped at 800 characters** (about 50 seconds of speech), and the
cap exists for that reason: a longer turn cannot be delivered inside Vapi's
30-second deadline, so it would be cut off mid-word and billed anyway. If your
agent produces longer replies, split them — Vapi will request each piece
separately and the caller hears them back to back.

If no worker starts answering within 12 seconds, the request fails with a `503`
rather than holding the socket until Vapi gives up. That distinction matters:
a status code your `fallbackPlan` can act on beats a timeout, which just looks
like a provider that stopped responding.

Set a `fallbackPlan` on the assistant if a missed utterance would be worse than a
non-Vietnamese voice.

## Troubleshooting

| Symptom | Cause |
|---|---|
| `401` | The `secret` is not a valid VieNeu API key, or the key was revoked. |
| `400` with a voice message | `voiceId` is not in the catalogue for that engine — check `GET /v1/audio/voices`. |
| `403` | The key's token grant is empty or expired, or your plan excludes the engine. |
| `400` about `message.text` length | The turn was over 800 characters — split it. |
| `503` naming a sample rate | No worker in the pool has been updated to encode raw PCM yet. We fail over across the fleet first, so this means all of them. Retry; contact us if it persists. |
| `503` about no worker answering | Nothing started producing audio within 12 seconds. Usually a capacity spike; your `fallbackPlan` covers the turn. |
| `502` about no audio | The worker accepted the request and then produced nothing. Not billed. |

Every response carries `X-Request-Id`. Quote it and we can find the exact call.
