← All docs

VaaniYantra — Developer Guide

This guide covers building on VaaniYantra (the public REST API and webhooks) and working in the codebase (architecture, local setup, deployment).

  • End-user docs: User Guide
  • Base URL: https://vaaniyantra.com

Table of contents

Integrating with the API

  1. Authentication
  2. REST API reference
  3. Webhooks

Working on the codebase 4. Architecture overview 5. Tech stack 6. Local development 7. Project structure 8. Telephony & the voice pipeline 9. Deployment


Integrating with the API

1. Authentication

Create an API key in Settings → API keys. Keys look like ck_live_… and are shown once — store it securely (it's hashed at rest, so it can't be retrieved later).

Send it on every request as a Bearer token or an x-api-key header:

curl https://vaaniyantra.com/api/v1/agents \
  -H "Authorization: Bearer ck_live_xxxxxxxxxxxxxxxx"

# equivalently:
curl https://vaaniyantra.com/api/v1/agents \
  -H "x-api-key: ck_live_xxxxxxxxxxxxxxxx"
  • All endpoints are scoped to the organization that owns the key.
  • Missing/invalid/revoked/expired keys return 401 {"error":"Invalid or missing API key"}.
  • Revoke a key anytime from Settings → API keys.

2. REST API reference

Base path: https://vaaniyantra.com/api/v1. All responses are JSON. List endpoints accept ?limit= (default 25, max 100) and ?offset=.

Agents

GET /api/v1/agents — list agents.

{ "agents": [
  { "id": "…", "name": "Hindi Helpdesk", "language": "hi-IN",
    "voice": "Aoede", "status": "ACTIVE", "direction": "BOTH" }
]}

GET /api/v1/agents/:id — one agent (adds greeting, createdAt).

{ "agent": { "id": "…", "name": "…", "language": "en-IN", "voice": "…",
  "status": "ACTIVE", "direction": "BOTH", "greeting": "Hello!…",
  "createdAt": "2026-07-10T…" } }

Calls

GET /api/v1/calls — list calls. Query filters: direction (INBOUND|OUTBOUND), status (COMPLETED|FAILED|NO_ANSWER|…), agentId, plus limit/offset.

{ "calls": [ { …call… } ], "total": 128, "limit": 25, "offset": 0 }

GET /api/v1/calls/:id — one call with full transcript.

{ "call": {
  "id": "…", "direction": "INBOUND", "status": "COMPLETED",
  "fromNumber": "+9198…", "toNumber": "+9180…", "durationSec": 92,
  "language": "en-IN", "agent": { "id": "…", "name": "…" },
  "summary": "Caller asked about pricing and booked a follow-up.",
  "sentiment": "POSITIVE", "sentimentScore": 0.6,
  "topics": ["pricing","follow-up"],
  "collectedData": { "name": "Alex", "preferred_time": "Tomorrow 3pm" },
  "recordingUrl": "/api/calls/…/recording",
  "createdAt": "…", "startedAt": "…", "endedAt": "…",
  "transcript": [ { "role": "AGENT", "text": "Hello!…", "atMs": 300 },
                  { "role": "CALLER", "text": "Hi…", "atMs": 2100 } ]
}}

POST /api/v1/calls — place an outbound call.

curl -X POST https://vaaniyantra.com/api/v1/calls \
  -H "Authorization: Bearer ck_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "AGENT_ID", "to": "+919876543210", "fromNumberId": "NUMBER_ID" }'
FieldRequiredNotes
agentIdyesAn agent in your org.
toyesDestination in E.164.
fromNumberIdnoWhich of your numbers to call from (else a default caller ID).

Returns 201 with the created call object. Errors: 400 (bad input), 403 (monthly call-minute limit reached), 404 (agent not found), 502 (provider failed). Outbound is subject to your plan's call-minute limit.

Numbers

GET /api/v1/numbers — list your phone numbers.

{ "numbers": [
  { "id": "…", "e164": "+916624394745", "label": "Main line",
    "provider": "twilio", "agentId": "…", "inboundEnabled": true,
    "webhookConfigured": true, "createdAt": "…" }
]}

Appointments

GET /api/v1/appointments — list appointments. Query filters: status (CONFIRMED|CANCELLED), upcoming=true, limit/offset.

{ "appointments": [
  { "id": "…", "customerName": "Alex", "customerNumber": "+9198…",
    "service": "consultation", "expertName": "Dr. Rao",
    "startAt": "…", "endAt": "…", "timezone": "Asia/Kolkata",
    "status": "CONFIRMED", "channel": "sms", "agentId": "…", "createdAt": "…" }
], "total": 12, "limit": 25, "offset": 0 }

3. Webhooks

Register endpoints in Settings → Webhooks. When a subscribed event fires, we POST a JSON payload to your URL.

Events

EventWhen
call.completedA call finished successfully (includes summary, sentiment, collected data).
call.failedA call ended without connecting (failed, busy, no answer).

Delivery & headers

  • Method: POST, Content-Type: application/json.
  • X-VaaniYantra-Event: call.completed
  • X-VaaniYantra-Signature: sha256=<hex HMAC-SHA256 of the raw body>
  • Redirects are not followed, and endpoints must resolve to a public address (internal/loopback addresses are blocked). 6-second timeout.
  • Each webhook has a signing secret (whsec_…) shown in the UI. Delivery outcome (last status, last error) is recorded on the webhook.
  • Slack webhooks (type slack) receive a Block Kit message instead and are not HMAC-signed.

Payload

{
  "event": "call.completed",
  "timestamp": "2026-07-10T12:00:00.000Z",
  "data": {
    "id": "call_…", "direction": "INBOUND", "status": "COMPLETED",
    "fromNumber": "+9198…", "toNumber": "+9180…", "durationSec": 42,
    "language": "en-IN", "agent": { "id": "…", "name": "…" },
    "summary": "…", "sentiment": "POSITIVE", "sentimentScore": 0.6,
    "topics": ["pricing"], "collectedData": { "name": "Alex" },
    "recordingUrl": "https://vaaniyantra.com/api/calls/…/recording",
    "createdAt": "…", "endedAt": "…"
  }
}

Verifying the signature (Node.js)

import crypto from 'crypto';

function verify(rawBody, signatureHeader, secret) {
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}

// Express example — use the RAW body, not the parsed object:
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verify(req.body, req.get('X-VaaniYantra-Signature'), process.env.WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(401);
  const event = JSON.parse(req.body.toString());
  // … handle event …
  res.sendStatus(200);
});

Use the "Send test" button in the UI to deliver a sample payload and confirm your endpoint verifies it.


Working on the codebase

4. Architecture overview

Two Node processes back the platform:

  • vaani-web — the Next.js 14 app (App Router): dashboard UI + all /api/* routes. Port 3000.
  • vaani-voice — the realtime media server (voice-server/index.ts): the WebSocket endpoint telephony providers stream call audio to, bridged to the AI in real time. Port 8080.
Caller ─▶ Twilio / Exotel ─▶ (webhook/applet) ─▶ voice-server WS (:8080)
                                                   │  audio ⇄ Gemini Live
                                                   ▼
                                              PostgreSQL  ◀─▶  Next.js app (:3000)
  • Auth: Firebase Authentication (client SDK + Admin SDK for session cookies).
  • Data: PostgreSQL via Prisma.
  • Voice AI: Gemini Live (native speech-to-speech) is the default realtime path; a cascade path (Deepgram STT → LLM → TTS) exists as a fallback.
  • Telephony: provider-agnostic adapters (Twilio, Exotel, mock).

5. Tech stack

AreaChoice
FrameworkNext.js 14 (App Router), React 18, TypeScript
StylingTailwind CSS
DB / ORMPostgreSQL + Prisma
AuthFirebase Auth (firebase, firebase-admin)
Realtime voice@google/genai (Gemini Live), ws
STT / TTSDeepgram, Google Cloud Speech/TTS, ElevenLabs (optional)
TelephonyTwilio REST, Exotel (Voicebot streaming)
PaymentsCashfree Subscriptions (REST, no SDK)
ValidationZod

6. Local development

Prerequisites: Node 20+, a PostgreSQL database.

# 1. Install
npm install

# 2. Configure env
cp .env.example .env       # then fill in real values (see below)

# 3. Set up the database
npm run db:push            # apply the Prisma schema
npm run db:seed            # optional: seed demo data

# 4. Run (two processes)
npm run dev                # Next.js app on :3000
npm run voice-server       # realtime voice server on :8080

Mock mode: with USE_MOCK_TELEPHONY/SPEECH/LLM="true" the app runs fully offline with deterministic mock providers — no external keys needed. Flip them to "false" and add real keys to go live.

Key environment variables (see .env.example for the full list):

VarPurpose
DATABASE_URLPostgres connection string
APP_BASE_URL / PUBLIC_BASE_URLPublic URLs used to build webhook URLs
VOICE_WS_URL / VOICE_WS_PORTRealtime voice-server WS URL / port
GEMINI_API_KEYGemini (LLM + Live voice)
GOOGLE_CLOUD_API_KEYGoogle Speech-to-Text / Text-to-Speech
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_CALLER_IDPlatform Twilio
NEXT_PUBLIC_FIREBASE_*Firebase client config (public)
FIREBASE_* (admin)Firebase Admin credentials (server)
GOOGLE_OAUTH_CLIENT_ID / _SECRETGoogle Calendar/Drive connector
DEEPGRAM_API_KEY, ELEVENLABS_API_KEYOptional STT/TTS providers
SUPERADMIN_EMAILSComma-separated founder emails for /admin

⚠️ Never commit real secrets. .env, Exotel.env, *.db, and build output are git-ignored. .env.example must contain placeholders only.

Useful scripts: npm run build, npm run start, npm run lint, npm run db:studio (Prisma Studio), npm run db:generate.

7. Project structure

src/
  app/                     Next.js App Router
    (app)/…                dashboard pages (agents, numbers, calls, settings…)
    (auth)/…               sign-in / sign-up
    api/                   REST endpoints
      v1/                  ← public API (agents, calls, numbers, appointments)
      telephony/           Twilio + Exotel webhooks / stream URL
      webhooks/, keys/     outbound webhooks + API keys
      admin/               founder-only platform config
  components/              React UI (AgentForm, NumbersManager, PricingGrid, …)
  lib/
    billing.ts             plan catalogue + limits/usage enforcement
    api-keys.ts            API-key generation + authentication
    webhooks.ts            signing, delivery, dispatch, SSRF guard
    telephony-accounts.ts  BYO Twilio/Exotel account resolution
    adapters/telephony/    TelephonyProvider: twilio.ts, exotel.ts, mock.ts
    voice/                 codec.ts, live.ts (Gemini Live), deepgram.ts, tts.ts…
    calls/place-outbound.ts shared outbound-call placement
    connectors.ts, crm.ts  Google/SMS/HubSpot integrations
    auth/                  Firebase session, workspace context, RBAC
prisma/schema.prisma       database schema
voice-server/index.ts      realtime media server (WS :8080)

8. Telephony & the voice pipeline

Providers implement the TelephonyProvider interface (src/lib/adapters/telephony/types.ts): provisionNumber, listAccountNumbers, configureNumberWebhook, placeCall, buildAnswerDocument, redirectCall, plus capability flags (canBuyNumbers, usesAnswerWebhook).

  • Twilio — mulaw/8kHz audio over Media Streams; per-call TwiML; webhooks set automatically via the REST API on import.
  • Exotel — raw PCM16/8kHz audio over the Voicebot applet (configured once in the Exotel dashboard, pointed at wss://…/exotel); no per-call document.

Because the two differ in audio format, the voice pipeline is parameterised by a TelephonyCodec (src/lib/voice/codec.ts): MULAW_CODEC (Twilio) and PCM16_CODEC (Exotel). The voice-server upgrades both /twilio and /exotel WebSocket paths and picks the codec per connection; live.ts (Gemini Live) and the Deepgram/TTS cascade both consume the codec so neither provider hard-codes an audio format.

9. Deployment

Production runs on a GCP Compute Engine VM behind Caddy (auto-HTTPS), with pm2 managing vaani-web and vaani-voice, and self-hosted PostgreSQL on the box.

The deploy flow (no CI): build locally, tar the project (excluding node_modules, .next, .git, .env, and secret files), gcloud compute scp to the VM, extract, prisma db push if the schema changed, npm run build, then pm2 restart. Caddy routes /twilio, /exotel, /health, and the Twilio webhook paths to the voice-server (:8080); everything else to Next.js (:3000).

vaaniyantra.com is the canonical host: a separate Caddy site block 301-redirects www.vaaniyantra.com to the apex domain (path preserved) so search engines see a single host. The previous config is kept at /etc/caddy/Caddyfile.bak on the VM.

Note: pushing to GitHub does not auto-deploy — production deploys are deliberate (tar + scp + build + restart).


Questions or issues? See the User Guide for product behavior, or open an issue in the repository.