Call analytics platform · powered by APIs
Every sales call — transcribed and scored automatically.
Connect your call source once. From then on, the system transcribes conversations on its own, separates the rep from the customer, scores performance against your checklist, and rolls everything up into reports.
Getting started
Up and running in four steps
Connect your source once — everything after that runs without you.
-
01
Sign up
Create a workspace for your team and invite your sales managers.
-
02
Connect a source
Generate a webhook URL and secret, then drop them into your phone system or CRM. Recordings start flowing in automatically.
-
03
Set up your checklist
Define your scoring criteria: greeting, needs discovery, objection handling, agreeing on the next step.
-
04
Done
Every new call is transcribed, scored, and shows up in the dashboard with per-rep reports.
Demo
See how it works
Our demo of CallLens recognising a real call — and the example recording it ran on.
Screen recording: CallLens transcribes and scores a real call inside the cabinet.
Architecture
A call's journey through the system
Five processing zones. Synchronous intake on Symfony returns a response in milliseconds, while all the heavy lifting runs asynchronously in workers that call external APIs . No GPU of your own required.
-
A
Zone · Customeroutside your perimeter
Call source
- Source Twilio · Binotel · Ringostat · Bitrix24
- Event call completed
- Data mp3/wav recording, ideally dual-channel (rep/customer on separate tracks)
-
B
Zone · Symfony (synchronous)< 50 ms
Webhook intake
- Endpoint POST /v1/calls/webhook
- Security HMAC-SHA256 signature verification
- Dedupe idempotency by call_id
- Action store audio in Hetzner S3 → dispatch to Messenger
- Response 202 Accepted immediately, without waiting for processing
-
C
Zone · Workers (asynchronous)Messenger
Call processing
- Transport Redis / Doctrine · retries with backoff · dead-letter
- Calls via Symfony HttpClient behind a single interface
Pipeline · external APIspaid/min1 · STTDeepgram / AssemblyAI → transcript + diarization + word timestamps2 · LLMGPT-4o-mini / Gemini → JSON scoring per criterion + quotes (structured output)3 · EMBOpenAI / Voyage → utterance vectors for search -
D
Zone · DataDoctrine ORM
PostgreSQL + pgvector
- Entities Call · Transcript · Utterance(+vector) · Score · Tenant
- Isolation data partitioned by tenant_id (multi-tenancy)
- Search semantic vector search (pgvector, HNSW index)
- E
The amber zone C is the only one that depends on external paid APIs. Everything sits behind an interface, so you can swap providers and, down the line, move transcription onto your own GPU without touching zones A, B, D, or E.
Infrastructure decision
Why APIs instead of your own GPU
At launch and through early growth, paid APIs cost 3–4× less than running your own GPU — and they're far simpler to operate. A dedicated GPU only pays off at high volume.
Paid APIs
for 50,000 min/mo (≈ 10,000 calls)
- ✓Zero DevOps for inference
- ✓Pay only for what you actually use
- ✓Instant scaling under load
- ✓Launch in weeks, with no capital outlay
Your own GPU
for the same 50,000 min/mo volume
- ✕GPU sits idle ~96% of the time at low volume
- ✕Requires DevOps: vLLM, monitoring, model updates
- ✕Fixed cost regardless of load
- →Only breaks even from ~215,000 min/mo
Everything sits behind a single interface, so once volume crosses ~215,000 min/mo, we move transcription onto our own GPU with no system rewrite — architecture zones A, B, D, and E stay unchanged.
From sound to analytics
How a conversation becomes a number
Four transformations: raw call audio turns, step by step, into a structured rep score and aggregated analytics.
Raw call audio recording
Transcript with diarization
"…I'll send over the proposal today" — supporting quote
Scored against your checklist
Average scores across the team
Reports and analytics
Metrics on top of PostgreSQL with Cube
Cube is a semantic layer over the database: metrics are defined once, then served through an API with pre-aggregations and caching. Reports stay fast on plain PostgreSQL — no separate query engine needed.
PostgreSQL
Call · Score · Utterance
Cube
pre-aggregations + cache
API
REST / GraphQL / SQL
Next.js
dashboards and reports
cubes: - name: calls sql_table: calls measures: - name: avg_score sql: score type: avg - name: count type: count dimensions: - name: manager sql: manager_id type: string - name: week sql: created_at type: time pre_aggregations: - name: by_manager_week measures: [avg_score, count] dimensions: [manager] granularity: week
Average score by rep
avg_scoreQuality trend by week
granularity: weekTop customer objections
countConnecting a source
What "registering a webhook" means
After signing up, head to Settings → Integrations and you'll get your own webhook URL and secret key. You paste this URL into your phone system or CRM.
When a call ends, your system sends a message to that URL with a link to the recording. Symfony verifies the signature, stores the audio, and queues the call for processing. One setup at the start — automatic from there on.
- ✓An HMAC-SHA256 signature guards against forged requests.
- ✓Dual-channel recordings put the rep and customer on separate tracks, with no diarization errors.
- ✓No webhook? You can upload recordings manually or via the REST API.
POST https://api.calllens.app/v1/calls/webhook X-CallLens-Signature: sha256=9f3a… { "call_id": "c_8421", "recording_url": "https://…/rec.mp3", "agent_id": "mgr_17", "channels": "dual", "started_at": "2026-06-17T09:14:02Z", "duration_sec": 312 }
#[Route]('/v1/calls/webhook', methods:['POST']) public function handle(Request $r): Response { $this->verifySignature($r); // HMAC $bus->dispatch(new ProcessCall($payload)); return new Response(status: 202); }
Infrastructure · hardware
One dedicated server is enough
All the heavy processing — transcription, scoring, embeddings — runs on external APIs, so the server needs no GPU. It only hosts the app, the workers, the database, and the queue — comfortably handled by a single Hetzner dedicated server.
Hetzner AX-line
- AMD Ryzen · 8 cores / 16 threads
- 64 GB DDR5 ECC
- 2 × 512 GB NVMe (RAID 1)
- 1 Gbit/s, unmetered traffic
+ ~€39 one-time (setup)
Hetzner Object Storage
- S3-compatible (works with the same code)
- EU data centers · GDPR (instead of Cloudflare)
- 1 TB storage + 1 TB traffic included
- beyond that — pay-as-you-go
S3 API calls and inbound traffic — free
server + storage
Why one server is enough
- ✓No GPU — just web, workers, database, queue
- ✓Workers wait on API responses (I/O), they don't tax the CPU
- ✓PostgreSQL + Redis run comfortably on 64 GB and NVMe
- →To scale: a bigger server, or add a dedicated worker
What's installed on the server
The full project stack on a single node (click for details):
A free web panel for server administration: web server, databases, domains, SSL, cron, and backups — all in a graphical interface, no manual console required. It also provides real-time server analytics.
Hetzner prices are shown excluding VAT and are approximate — check current configurations and pricing on the Hetzner website before ordering.
Tech stack
What it runs on
Click any technology to open its official website.
Symfony
Backend, REST API, multi-tenancy
Symfony Messenger
Asynchronous call-processing queue
Doctrine ORM
Mapping entities to PostgreSQL
PostgreSQL
Primary database: calls, scores, reports
pgvector
Utterance vectors and semantic search
Cube
Semantic layer and pre-aggregations for reports
Next.js
React-based dashboard frontend
Hetzner Object Storage
S3-compatible audio storage (EU, GDPR)
Twilio
Telephony and call recording
Symfony AI · HttpClient
External API calls behind a single interface
Deepgram
STT API: transcription with diarization
AssemblyAI
STT API: alternative
OpenAI
LLM scoring and embeddings
Google Gemini
LLM scoring (budget tier)
Voyage AI
Embeddings API for search
Ready to connect your sales team?
Sign up, webhook, checklist — and your first scores land today.