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

ComponentVapi fieldSCX.ai endpointModel ID
Speech‑to‑text (realtime)transcriberwss://api.scx.ai/v1/transcribers/vapiscx-stt
Chat / reasoningmodelhttps://api.scx.ai/v1any chat model on your account
Text‑to‑speechvoicehttps://api.scx.ai/v1/voices/vapiscx-tts

All three use the same SCX.ai API key, but how the key reaches us differs by component:

  • Voice (HTTP): Vapi forwards server.secret as the x-vapi-secret header.
  • Model (custom LLM, HTTP): Vapi sends the Authorization: Bearer … header you set in server.headers.
  • Transcriber (WebSocket): set the key in server.headers as Authorization: Bearer …. Do not rely on server.secret alone here — Vapi does not reliably forward x-vapi-secret on the WebSocket upgrade handshake, so a secret-only transcriber fails at call initialisation with vapifault 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:

  1. 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)
  2. Voice → Custom Voice
    • URL: https://api.scx.ai/v1/voices/vapi?model=scx-tts
    • Secret: your SCX.ai API key
  3. 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)

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." }
      ]
    }
  }'

Then attach an existing phone number:

bash
# 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.

bash
#!/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:

bash
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.

bash
# 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.