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
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" }'
| Field | Required | Notes |
|---|---|---|
agentId | yes | An agent in your org. |
to | yes | Destination in E.164. |
fromNumberId | no | Which 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
| Event | When |
|---|---|
call.completed | A call finished successfully (includes summary, sentiment, collected data). |
call.failed | A call ended without connecting (failed, busy, no answer). |
Delivery & headers
- Method:
POST,Content-Type: application/json. X-VaaniYantra-Event: call.completedX-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. Port3000.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. Port8080.
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
| Area | Choice |
|---|---|
| Framework | Next.js 14 (App Router), React 18, TypeScript |
| Styling | Tailwind CSS |
| DB / ORM | PostgreSQL + Prisma |
| Auth | Firebase Auth (firebase, firebase-admin) |
| Realtime voice | @google/genai (Gemini Live), ws |
| STT / TTS | Deepgram, Google Cloud Speech/TTS, ElevenLabs (optional) |
| Telephony | Twilio REST, Exotel (Voicebot streaming) |
| Payments | Cashfree Subscriptions (REST, no SDK) |
| Validation | Zod |
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):
| Var | Purpose |
|---|---|
DATABASE_URL | Postgres connection string |
APP_BASE_URL / PUBLIC_BASE_URL | Public URLs used to build webhook URLs |
VOICE_WS_URL / VOICE_WS_PORT | Realtime voice-server WS URL / port |
GEMINI_API_KEY | Gemini (LLM + Live voice) |
GOOGLE_CLOUD_API_KEY | Google Speech-to-Text / Text-to-Speech |
TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_CALLER_ID | Platform Twilio |
NEXT_PUBLIC_FIREBASE_* | Firebase client config (public) |
FIREBASE_* (admin) | Firebase Admin credentials (server) |
GOOGLE_OAUTH_CLIENT_ID / _SECRET | Google Calendar/Drive connector |
DEEPGRAM_API_KEY, ELEVENLABS_API_KEY | Optional STT/TTS providers |
SUPERADMIN_EMAILS | Comma-separated founder emails for /admin |
⚠️ Never commit real secrets.
.env,Exotel.env,*.db, and build output are git-ignored..env.examplemust 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.