Vaanieval
Quickstart

Node.js

Record your first voice-agent call from Node.js, with automatic fetch instrumentation and websocket accounting.

By the end of this page you will have a recorded call in a local dashboard, with audio you can play and a trace you can expand.

Before you start

  • Node.js 20 or newer (the SDK declares engines.node >= 20)
  • Python 3.11 or newer for the dashboard
  • git and network access to GitHub. The repository is public and supports anonymous Git access; this guide installs from Git. The anonymous npm registry check for @vaanieal/observer returned HTTP 404 on 20 September 2026; no release was found under that package name.

Source installs and hosted beta

The repositories are public and support anonymous Git access. These guides install the SDKs from public Git. Anonymous registry checks on 20 September 2026 returned HTTP 404 for npm @vaanieal/observer and PyPI vaanieval-observer; no release was found under those names. Source availability does not mean the hosted service is ready for public launch: new public workspaces remain closed.

The quickstarts pin published Python 0.5.7b1 and Node 0.1.1-beta.1 source previews. Both installed previews have uploaded synthetic calls to the isolated hosted development environment. Git/build tooling and explicit revision upgrades are required; this is not registry publication or production-readiness certification.

For setup help, email shubham@vaanieval.com or book a call — a short conversation about your use case tells us whether Vaanieval fits, and helps identify the setup that matches your stack.

For an admitted hosted workspace, use the same pinned SDK below and the three-step setup at beta.vaanieval.com. You do not need to run the local dashboard. Public workspace admissions remain closed. This is a source preview (0.1.1-beta.1), not an npm registry release; Git is required and upgrades are explicit.

1. Run the dashboard

Clone and install

git clone https://github.com/shubhamofbce/vaanieval-observer-backend.git dashboard
cd dashboard
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Start it

uvicorn app.main:app --host 127.0.0.1 --reload --port 8000

The console is at http://localhost:8000/.

Out of the box the local dashboard accepts any non-empty API key without checking it, and has no tenant isolation. Set VAANI_REQUIRE_API_KEY=1 to make ingest require a real key; read endpoints stay open either way. Keep it on localhost. A key does not make it tenant-safe; hosted beta is separate work under construction. See Self-hosting.

2. Install the SDK

npm install 'git+https://github.com/shubhamofbce/vaanieval-observer-nodejs-sdk.git#d2bb0f3629d724ea6bfc505e1b975d595dddddeb'

The SDK is pure ESM with no runtime dependencies — it uses only Node built-ins (node:async_hooks, node:fs/promises, node:crypto, node:perf_hooks).

package.json
{
  "type": "module"
}

3. Record a call

record-a-call.mjs
import { VaaniObserver } from '@vaanieal/observer';

const SAMPLE_RATE = 16000;
const FORMAT = { encoding: 'pcm_s16le', sampleRateHz: SAMPLE_RATE, channels: 1 };

/** A second of audible PCM, so the recording has something to play. */
function tone(hz, ms) {
  const frames = Math.floor((SAMPLE_RATE * ms) / 1000);
  const buffer = Buffer.alloc(frames * 2);
  for (let n = 0; n < frames; n += 1) {
    buffer.writeInt16LE(Math.round(9000 * Math.sin((2 * Math.PI * hz * n) / SAMPLE_RATE)), n * 2);
  }
  return buffer;
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const vaani = new VaaniObserver({
  endpoint: 'http://localhost:8000',
  apiKey: 'local-dev', // the local dashboard does not validate this
  spoolDirectory: './.vaani-spool',
  endpoints: [{ id: 'llm', type: 'llm', url: 'https://api.openai.com/v1' }],
});

const session = vaani.startSession({ agentId: 'quickstart-agent' });

for (let index = 0; index < 2; index += 1) {
  const turn = session.startTurn();

  // The caller speaks.
  const stt = turn.startOperation({ type: 'stt', provider: 'demo' });
  stt.event('speech_started');
  session.recordInboundAudio(tone(220, 900), FORMAT);
  await sleep(900);
  stt.event('final_transcript');
  stt.end({ status: 'ok', response: { transcript: `caller line ${index + 1}` } });

  // The model thinks.
  const llm = turn.startOperation({ type: 'llm', provider: 'demo', model: 'demo-model' });
  await sleep(400);
  llm.event('first_token');
  llm.end({ status: 'ok', response: { tokens: 128 } });

  // The agent speaks. `operationId` attributes the PCM to this exact reply.
  const tts = turn.startOperation({ type: 'tts', provider: 'demo' });
  session.recordOutboundAudio(tone(440, 1200), {
    ...FORMAT,
    turnId: turn.id,
    operationId: tts.id,
  });
  tts.end({ status: 'ok' });

  turn.end();
}

const finalized = await session.end({ outcome: 'completed' });
console.log('package written to', finalized.directory);

await vaani.uploadPackage(finalized);
console.log('uploaded session', finalized.sessionId);
node record-a-call.mjs

4. Open the call

Refresh http://localhost:8000/, click the call in the rail, and expand the trace.

An isolated localhost trace for synthetic-review with one visible tool span, one second of silent audio and no measured voice timing; no provider retries are shown.

The documentation screenshot is a separate synthetic, tool-only fixture, not the two-turn script above or a hosted customer call.

Automatic fetch capture

The Node SDK patches the global fetch when it is constructed. Any request made inside session.run(...) whose URL matches one of your endpoints rules is timed and recorded — no change at the call site:

const session = vaani.startSession({ agentId: 'support' });
const turn = session.startTurn();

await turn.run(async () => {
  // Recorded as an LLM operation on this turn, because api.openai.com
  // matches the configured endpoint rule.
  await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', body });
});

Three helpers scope that ambient context:

HelperUse it when
session.run(fn)Attribute work to this session
session.withTurn(turnId, fn)Group auto-instrumented calls under one turn
session.withEndpoint(id, fn, { turnId })Force a specific endpoint rule; throws on an unknown id

session.bind(handler) wraps a callback so it runs inside session.run — useful for event-emitter handlers.

Streaming response bodies are never drained to capture them. Doing so would hold back the first token, which is exactly the latency this SDK exists to measure.

Websockets

Most streaming STT and TTS providers hold one socket open for the whole call, so the SDK records it as a connection-scoped span rather than as a turn:

import WebSocket from 'ws';

const socket = new WebSocket('wss://api.deepgram.com/v1/listen');
const handle = vaani.observeWebSocket(socket, { session, url: socket.url });

// ... later
handle.detach();

It records connected, sent_frame and received_frame milestones with byte counts and a running total, plus the close code. Frame contents and authentication headers are never stored.

A session-long socket is recorded as a connection span, not a turn — per-turn work is recorded explicitly with session.startTurn() and turn.startOperation(). See Sessions, turns and operations.

What just happened

The observer installed instrumentation, not a connection

new VaaniObserver(...) validates your endpoint rules and patches global fetch. endpoint and apiKey are used only by uploadPackage().

The session spooled to disk during the call

Events and audio were appended to ./.vaani-spool/<session-id>/ through a promise chain that preserves strict append ordering.

end() finalized the package

Open spans were closed, the two mono tracks were composed into one timeline-aligned stereo call.audio (agent left, caller right), and manifest.json was written last.

uploadPackage() shipped it, after the call

POST /v1/sessions → PUT each object → POST .../complete with byte sizes and SHA-256 digests. Upload is explicit and post-call; the library never uploads on the live media path.

Troubleshooting

Next

On this page