Vapi
Vapi assistants run on three swappable components: a transcriber (speech‑in), a model (chat), and a voice (speech‑out). SCX.ai exposes drop‑in endpoints for all three so a single API key can power your entire voice agent.
This guide walks through the configuration end‑to‑end and includes a script that wires it up via the Vapi REST API.
What you'll connect
| Component | Vapi field | SCX.ai endpoint | Model ID |
|---|---|---|---|
| Speech‑to‑text (realtime) | transcriber | wss://api.scx.ai/v1/transcribers/vapi | scx-stt |
| Chat / reasoning | model | https://api.scx.ai/v1 | any chat model on your account |
| Text‑to‑speech | voice | https://api.scx.ai/v1/voices/vapi | scx-tts |
All three use the same SCX.ai API key, but how the key reaches us differs by component:
- Voice (HTTP): Vapi forwards
server.secretas thex-vapi-secretheader. - Model (custom LLM, HTTP): Vapi sends the
Authorization: Bearer …header you set inserver.headers. - Transcriber (WebSocket): set the key in
server.headersasAuthorization: Bearer …. Do not rely onserver.secretalone here — Vapi does not reliably forwardx-vapi-secreton the WebSocket upgrade handshake, so a secret-only transcriber fails at call initialisation withvapifault custom transcriber failed.
SCX.ai accepts Authorization: Bearer …, x-api-key, and x-vapi-secret interchangeably on every endpoint — so the rule of thumb is simply: always set Authorization: Bearer in server.headers on the transcriber.
Prerequisites
- A SCX.ai API key — create one in the dashboard under API Keys.
- A Vapi account with a private API key (Settings → API Keys → "Private Key").
- A phone number provisioned in Vapi (BYO Twilio/Telnyx or a Vapi‑managed number).
Manual configuration in the Vapi dashboard
If you'd rather click through the UI, point each component at the URLs and model IDs above. The transcriber and voice both use the Custom provider:
- Transcriber → Custom Transcriber
- URL:
wss://api.scx.ai/v1/transcribers/vapi?model=scx-stt - Secret: your SCX.ai API key
- Headers: add
Authorization=Bearer YOUR_API_KEY— required for the transcriber (see the auth note above; the secret field alone does not authenticate the WebSocket upgrade)
- URL:
- Voice → Custom Voice
- URL:
https://api.scx.ai/v1/voices/vapi?model=scx-tts - Secret: your SCX.ai API key
- URL:
- Model → Custom LLM
- URL:
https://api.scx.ai/v1 - Authorization header:
Bearer YOUR_API_KEY - Model: pick from your available chat models (e.g.
MiniMax-M2.5)
- URL:
That's it — assign the assistant to your phone number and the next inbound call routes through SCX.ai.
Configure via the Vapi API
Three calls: create the assistant, attach the phone number, done.
# 1. Create the assistant
curl -X POST "https://api.vapi.ai/assistant" \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "SCX.ai Voice Agent",
"firstMessage": "Hi, you have reached SCX.ai. How can I help?",
"transcriber": {
"provider": "custom-transcriber",
"server": {
"url": "wss://api.scx.ai/v1/transcribers/vapi?model=scx-stt",
"secret": "your-scx-api-key",
"headers": { "Authorization": "Bearer your-scx-api-key" },
"timeoutSeconds": 30
}
},
"voice": {
"provider": "custom-voice",
"server": {
"url": "https://api.scx.ai/v1/voices/vapi?model=scx-tts",
"secret": "your-scx-api-key",
"timeoutSeconds": 30
}
},
"model": {
"provider": "custom-llm",
"url": "https://api.scx.ai/v1",
"model": "MiniMax-M2.5",
"headers": { "Authorization": "Bearer your-scx-api-key" },
"messages": [
{ "role": "system", "content": "You are a helpful voice assistant. Keep replies under two sentences." }
]
}
}'
# 1. Create the assistant
curl -X POST "https://api.vapi.ai/assistant" \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "SCX.ai Voice Agent",
"firstMessage": "Hi, you have reached SCX.ai. How can I help?",
"transcriber": {
"provider": "custom-transcriber",
"server": {
"url": "wss://api.scx.ai/v1/transcribers/vapi?model=scx-stt",
"secret": "your-scx-api-key",
"headers": { "Authorization": "Bearer your-scx-api-key" },
"timeoutSeconds": 30
}
},
"voice": {
"provider": "custom-voice",
"server": {
"url": "https://api.scx.ai/v1/voices/vapi?model=scx-tts",
"secret": "your-scx-api-key",
"timeoutSeconds": 30
}
},
"model": {
"provider": "custom-llm",
"url": "https://api.scx.ai/v1",
"model": "MiniMax-M2.5",
"headers": { "Authorization": "Bearer your-scx-api-key" },
"messages": [
{ "role": "system", "content": "You are a helpful voice assistant. Keep replies under two sentences." }
]
}
}'
Then attach an existing phone number:
# 2. Bind the assistant to a phone number
curl -X PATCH "https://api.vapi.ai/phone-number/$VAPI_PHONE_ID" \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"assistantId\": \"$ASSISTANT_ID\"}"
# 2. Bind the assistant to a phone number
curl -X PATCH "https://api.vapi.ai/phone-number/$VAPI_PHONE_ID" \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"assistantId\": \"$ASSISTANT_ID\"}"
Find $VAPI_PHONE_ID with curl -H "Authorization: Bearer $VAPI_API_KEY" https://api.vapi.ai/phone-number and pick the id of the number you want.
One‑shot setup script
Drop this into your repo as scripts/setup-vapi.sh. It creates the assistant, picks the first available phone number, and binds them. Idempotent on re‑run — passes the existing assistant id instead of creating duplicates.
#!/usr/bin/env bash
set -euo pipefail
: "${VAPI_API_KEY:?Set VAPI_API_KEY}"
: "${SCX_API_KEY:?Set SCX_API_KEY}"
NAME="${ASSISTANT_NAME:-SCX.ai Voice Agent}"
SCX_HOST="${SCX_HOST:-api.scx.ai}"
SCX_BASE="${SCX_BASE:-https://api.scx.ai/v1}"
CHAT_MODEL="${CHAT_MODEL:-MiniMax-M2.5}"
H_VAPI=(-H "Authorization: Bearer $VAPI_API_KEY" -H "Content-Type: application/json")
# Find or create the assistant by name.
ASSISTANT_ID=$(curl -fsS "${H_VAPI[@]}" "https://api.vapi.ai/assistant?limit=100" \
| jq -r --arg n "$NAME" '.[] | select(.name == $n) | .id' | head -n1)
PAYLOAD=$(jq -n \
--arg name "$NAME" \
--arg host "$SCX_HOST" \
--arg base "$SCX_BASE" \
--arg key "$SCX_API_KEY" \
--arg model "$CHAT_MODEL" \
'{
name: $name,
firstMessage: "Hi, you have reached \($name). How can I help?",
transcriber: { provider: "custom-transcriber", server: {
url: "wss://\($host)/v1/transcribers/vapi?model=scx-stt",
secret: $key, headers: { Authorization: "Bearer \($key)" }, timeoutSeconds: 30 } },
voice: { provider: "custom-voice", server: {
url: "https://\($host)/v1/voices/vapi?model=scx-tts",
secret: $key, timeoutSeconds: 30 } },
model: { provider: "custom-llm", url: $base, model: $model,
headers: { Authorization: "Bearer \($key)" },
messages: [{ role: "system", content: "You are a helpful voice assistant. Keep replies under two sentences." }] }
}')
if [[ -z "$ASSISTANT_ID" ]]; then
ASSISTANT_ID=$(curl -fsS -X POST "${H_VAPI[@]}" -d "$PAYLOAD" \
"https://api.vapi.ai/assistant" | jq -r '.id')
echo "created assistant $ASSISTANT_ID"
else
curl -fsS -X PATCH "${H_VAPI[@]}" -d "$PAYLOAD" \
"https://api.vapi.ai/assistant/$ASSISTANT_ID" >/dev/null
echo "updated assistant $ASSISTANT_ID"
fi
# Attach the first unassigned number, or use VAPI_PHONE_ID if set.
PHONE_ID="${VAPI_PHONE_ID:-$(curl -fsS "${H_VAPI[@]}" \
"https://api.vapi.ai/phone-number?limit=100" \
| jq -r '.[] | select(.assistantId == null) | .id' | head -n1)}"
if [[ -z "$PHONE_ID" ]]; then
echo "no available phone number — provision one in Vapi first" >&2
exit 1
fi
curl -fsS -X PATCH "${H_VAPI[@]}" \
-d "{\"assistantId\":\"$ASSISTANT_ID\"}" \
"https://api.vapi.ai/phone-number/$PHONE_ID" >/dev/null
echo "bound $PHONE_ID -> $ASSISTANT_ID"
#!/usr/bin/env bash
set -euo pipefail
: "${VAPI_API_KEY:?Set VAPI_API_KEY}"
: "${SCX_API_KEY:?Set SCX_API_KEY}"
NAME="${ASSISTANT_NAME:-SCX.ai Voice Agent}"
SCX_HOST="${SCX_HOST:-api.scx.ai}"
SCX_BASE="${SCX_BASE:-https://api.scx.ai/v1}"
CHAT_MODEL="${CHAT_MODEL:-MiniMax-M2.5}"
H_VAPI=(-H "Authorization: Bearer $VAPI_API_KEY" -H "Content-Type: application/json")
# Find or create the assistant by name.
ASSISTANT_ID=$(curl -fsS "${H_VAPI[@]}" "https://api.vapi.ai/assistant?limit=100" \
| jq -r --arg n "$NAME" '.[] | select(.name == $n) | .id' | head -n1)
PAYLOAD=$(jq -n \
--arg name "$NAME" \
--arg host "$SCX_HOST" \
--arg base "$SCX_BASE" \
--arg key "$SCX_API_KEY" \
--arg model "$CHAT_MODEL" \
'{
name: $name,
firstMessage: "Hi, you have reached \($name). How can I help?",
transcriber: { provider: "custom-transcriber", server: {
url: "wss://\($host)/v1/transcribers/vapi?model=scx-stt",
secret: $key, headers: { Authorization: "Bearer \($key)" }, timeoutSeconds: 30 } },
voice: { provider: "custom-voice", server: {
url: "https://\($host)/v1/voices/vapi?model=scx-tts",
secret: $key, timeoutSeconds: 30 } },
model: { provider: "custom-llm", url: $base, model: $model,
headers: { Authorization: "Bearer \($key)" },
messages: [{ role: "system", content: "You are a helpful voice assistant. Keep replies under two sentences." }] }
}')
if [[ -z "$ASSISTANT_ID" ]]; then
ASSISTANT_ID=$(curl -fsS -X POST "${H_VAPI[@]}" -d "$PAYLOAD" \
"https://api.vapi.ai/assistant" | jq -r '.id')
echo "created assistant $ASSISTANT_ID"
else
curl -fsS -X PATCH "${H_VAPI[@]}" -d "$PAYLOAD" \
"https://api.vapi.ai/assistant/$ASSISTANT_ID" >/dev/null
echo "updated assistant $ASSISTANT_ID"
fi
# Attach the first unassigned number, or use VAPI_PHONE_ID if set.
PHONE_ID="${VAPI_PHONE_ID:-$(curl -fsS "${H_VAPI[@]}" \
"https://api.vapi.ai/phone-number?limit=100" \
| jq -r '.[] | select(.assistantId == null) | .id' | head -n1)}"
if [[ -z "$PHONE_ID" ]]; then
echo "no available phone number — provision one in Vapi first" >&2
exit 1
fi
curl -fsS -X PATCH "${H_VAPI[@]}" \
-d "{\"assistantId\":\"$ASSISTANT_ID\"}" \
"https://api.vapi.ai/phone-number/$PHONE_ID" >/dev/null
echo "bound $PHONE_ID -> $ASSISTANT_ID"
Run it:
export VAPI_API_KEY=... # Vapi private key
export SCX_API_KEY=your-scx-api-key
bash scripts/setup-vapi.sh
export VAPI_API_KEY=... # Vapi private key
export SCX_API_KEY=your-scx-api-key
bash scripts/setup-vapi.sh
Verify the wiring
Before calling the number, hit each endpoint directly to confirm auth and routing.
# Chat
curl -sS "https://api.scx.ai/v1/chat/completions" \
-H "Authorization: Bearer $SCX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"MiniMax-M2.5","messages":[{"role":"user","content":"Say hi."}]}' \
| jq '.choices[0].message.content'
# TTS — Vapi-style POST returns raw 16-bit signed LE PCM @ the requested sampleRate.
curl -sS -X POST "https://api.scx.ai/v1/voices/vapi?model=scx-tts" \
-H "Authorization: Bearer $SCX_API_KEY" \
-H "Content-Type: application/json" \
-o /tmp/hello.pcm \
-d '{"message":{"type":"voice-request","text":"Hello.","sampleRate":24000,
"timestamp":1700000000000,"call":{"id":"x","orgId":"y"},
"assistant":{"id":"x","name":"y"}}}'
ls -lh /tmp/hello.pcm
# ASR — confirm the WebSocket upgrade succeeds. Easiest from a Node REPL
# or a tool like `wscat`:
# wscat -H "Authorization: Bearer $SCX_API_KEY" \
# -c "wss://api.scx.ai/v1/transcribers/vapi?model=scx-stt"
# A successful response is HTTP 101 Switching Protocols; a 401 means the
# bearer token wasn't accepted.
# Chat
curl -sS "https://api.scx.ai/v1/chat/completions" \
-H "Authorization: Bearer $SCX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"MiniMax-M2.5","messages":[{"role":"user","content":"Say hi."}]}' \
| jq '.choices[0].message.content'
# TTS — Vapi-style POST returns raw 16-bit signed LE PCM @ the requested sampleRate.
curl -sS -X POST "https://api.scx.ai/v1/voices/vapi?model=scx-tts" \
-H "Authorization: Bearer $SCX_API_KEY" \
-H "Content-Type: application/json" \
-o /tmp/hello.pcm \
-d '{"message":{"type":"voice-request","text":"Hello.","sampleRate":24000,
"timestamp":1700000000000,"call":{"id":"x","orgId":"y"},
"assistant":{"id":"x","name":"y"}}}'
ls -lh /tmp/hello.pcm
# ASR — confirm the WebSocket upgrade succeeds. Easiest from a Node REPL
# or a tool like `wscat`:
# wscat -H "Authorization: Bearer $SCX_API_KEY" \
# -c "wss://api.scx.ai/v1/transcribers/vapi?model=scx-stt"
# A successful response is HTTP 101 Switching Protocols; a 401 means the
# bearer token wasn't accepted.
Once those each succeed, dial the bound phone number — Vapi plays your firstMessage, the caller speaks, and the loop runs through SCX.ai's ASR → chat → TTS pipeline turn‑by‑turn.
What gets logged
Every request from Vapi shows up in your SCX.ai usage and request logs:
POST /v1/voices/vapi— one row per TTS turn, including character count and audio bytes returned.POST /v1/transcribers/vapi— one row per customer turn, with the final transcript and token usage. Only the customer channel is transcribed; the assistant channel carries the bot's own TTS and is intentionally not transcribed.POST /v1/chat/completions— one row per LLM call, with prompt and completion tokens.
Filter by endpoint=/v1/transcribers/vapi or endpoint=/v1/voices/vapi in the dashboard's request log to see exactly what Vapi is sending and what SCX.ai is returning for each call.