# Callia: a multi-tenant voice-agent platform you own and rebrand

> /api/voice/webhook handles call completion: it computes duration, deducts the minutes, records the disposition, and fires the automation events. It shipped without signature verification.

Source: https://saascode.ai/inside/road-to-callia · Published: 2026-08-09 · Section: builds · Product: Callia (https://saascode.ai/products/callia)

---
The public webhook that was also the billing meter.

`/api/voice/webhook` handles call completion: it computes duration, deducts the minutes, records the disposition, and fires the automation events. It shipped without signature verification. The design document had already named the guard — "provider signature" — in the table that listed every public route beside the mechanism that should protect it. The code did not have one. Callia is a self-hosted, multi-provider voice agent platform; its billing unit is the minute, which made the unauthenticated endpoint and the money meter the same endpoint.

## Starting point

The scope was written before the first line of project code. Voice only, by definition rather than by effort — SMS, WhatsApp and every other channel were recorded as separate future products, not deferred features. The flow designer's outbound action node was constrained to a generic webhook precisely so nobody could read it as a messaging capability. Video calls, SIP-trunking, IVR-menu building, and custom voice-model training were also out of scope, not roadmap.

Five provider layers were declared at the outset: telephony, speech-to-text, text-to-speech, an LLM router, and a middleware option that can substitute for the first three. Two pipelines followed: middleware mode (the v1 default) and direct mode — a separate deployment unit the main build is forbidden to depend on.

The credit unit was fixed before any schema work: one minute, rounded up using `Math.ceil(seconds / 60)`. Not tokens, not calls, not seconds.

An earlier voice product in the same lineage served as an architecture reference, not a code source. Four of its known defects were written into the plan as things the new build had to prove it did not repeat: a duplicate tenancy helper in the RLS policies, organization-scoped routes that did not filter by organization, a stub campaign engine, and a do-not-call list that was never checked before dialing.

## Reading the market

Research surveyed eleven platforms with all-in per-minute rates and columns for self-hosting, white-labeling, and multi-provider support. The conclusion was that a crowded category is a green light — validated demand, not a warning. Two claims inherited from the prior product were checked against that survey and marked false: that there were zero productized self-hosted competitors, and that this would be the first platform of its kind. Named counterexamples were cited. A second pair of headline figures was dropped as unverifiable. "Only / first / unique / no one else" was written into the project rules as prohibited language.

The multi-provider claim required a narrower reading than the tagline implied. Bring-your-own-keys is documented on the incumbents' own pricing pages — across speech, LLM, and voice. The defensible version of the claim is more specific: bring-your-own speech-to-text and text-to-speech separately, with the ability to swap them independently, is rarer. What the research validated with more confidence was ownership — the operator holds the platform asset and resells under their own brand and margin.

The per-minute cost does not disappear in self-hosted operation — telephony, speech-to-text, text-to-speech, and LLM inference are each metered, and a self-hosted operator pays all of them indefinitely. Self-hosting removes the platform fee layered on top: roughly 42–51% against a mid configuration (around $680 per month at ten thousand minutes on a representative stack), up to 73% against a lean one, and as little as 24% against a premium one, because the irreducible provider cost dominates as the stack gets more expensive. Against a platform that already permits bring-your-own-keys, the saving is exactly that platform's per-minute charge.

The research named the nearest honest comparison: a permissively-licensed open-source project, active and widely used, that describes itself in almost the same terms — acknowledging it was not a choice. The validated buyer was the reselling operator: agencies and businesses running their own voice platform for clients, where the confirmed pains were white-label add-on costs, provider lock-in, and capability withheld until enterprise tier.

## The decisions that shaped it

One minute as the billing unit determined the shape of the deduction. The database function that subtracts from an organization's balance is a single `UPDATE … RETURNING` statement, written that way to survive concurrent calls ending at the same moment without a read-modify-write race. Failures are retried once, then written to the audit log and surfaced as a payment-failure event — never swallowed. The call record is persisted before the ledger write, so the record stays accurate even if the deduction is delayed.

The knowledge-base search function bypasses row-level security by design. Per-agent retrieval runs on pgvector with a `SECURITY DEFINER` cosine search function that executes with the definer's rights and filters by agent only — RLS is not applied inside it, and the organization ownership check lives in the calling route, ahead of the search. A request for another organization's agent returns 404 before any embedding is computed.

The inherited tenancy defect was closed and tested adversarially. All row-level-security policies use one canonical tenancy helper — thirty-six uses across the seventeen new tables, with the prior product's duplicate helper present only as a warning comment. The post-build debug pass authenticated as a member of one organization and requested a foreign agent's detail page and its flow. Both returned 404.

Embeddings do not go through the LLM router. A prior build in the same codebase family measured roughly a 60% failure rate when embeddings were proxied through a router of the same design; the decision here was to call the embedding provider directly, with the router kept as a one-retry fallback. The cost is a second API key the operator must supply — one that is easy to confuse with the first.

The do-not-call list is checked at campaign build time and again at every individual dial job — two checks because the list can change between building a campaign and dialing its hundredth number. Every direct-mode path returns `503` with a `worker_required` flag when the relay is absent.

## What fought back

Twelve hand-designed page files were on disk. The index that maps each design file to a route had never been generated, so the initial build fell back to generic component defaults for all thirty-two pages and compiled cleanly. Nothing failed. Detection was a single grep: the dashboard page contained zero design markers where it should have contained several. After the index was regenerated and the twelve routes rebuilt from their design files, the same grep returned seven. Seventeen routes stayed on generic defaults, which was correct for them.

The webhook gap was the run's only critical security finding and blocked remaining work until it was resolved. The fix reads the raw request body before parsing — re-serializing would change the bytes the provider signed — verifies an HMAC-SHA256 digest with a constant-time comparison, supports a relay secret as a separate trust path, and fails closed: an unset secret returns 503 rather than processing an unsigned billing event. The re-scan enumerated every line that runs before the guard to confirm none mutate anything: a raw-body read, a JSON parse, a provider-slug lookup, a payload normalizer, and one read-only database query to resolve which organization the call belongs to.

A minutes pack purchase would have credited zero minutes. The monthly refill read a hardcoded allocation key belonging to a different product's credit unit, and the one-time purchase handler expected a configuration shape that did not match how the minute packs were seeded. Either gap alone silently credits nothing — a purchase that takes the money and delivers no minutes. Both were closed in the same pass.

The atomic deduction removed a floor the generic function it replaced had carried. The generic function reads the balance, throws when it is insufficient, then writes — exactly the race the atomic version exists to prevent. The atomic version subtracts unconditionally; if no balance row exists, it inserts a negative one. No path that starts a call reads the balance first. A low-balance event fires at 10% of the plan allowance, but it is a notification, not a gate. There is no record of a decision about the missing pre-flight check.

The design specification and the flow designer disagreed. The design described the call-flow page as a vertical step list, explicitly replacing any node-graph concept; the flow designer was a canvas with seven node types. The decision went against the design; the canvas was kept and re-skinned.

The design was redirected mid-build from dark to light — the superseded version was never retracted, only overruled. Nine days after release, an automated readiness check read the stale record and directed the operator to verify a dark landing page on a product that is deliberately light.

On the deployment host, the migration tool exits silently; migrations ran directly rather than through the tool — a fallback that never writes the tool's own registry table. The verification script queried that table, hit a missing-relation error, and crashed before reporting; the database was in perfect health. Separately, the dev-server launcher reported ready while requests returned nothing; a stale lock file from a prior dead instance was blocking startup underneath.

No provider keys were supplied, so nothing that makes a call was exercised end to end — all bring-your-own-key paths were empty. One consequence was caught and fixed: an unconfigured middleware key made a test call return a raw 500; it now returns 502. The audio path was not driven by automated tests; the relay ships as a 73-line WebSocket scaffold. The deployment region was never pinned — functions default to US East, adding latency to every sequential database call on a voice product — and it was logged as an operator action, not fixed.

## What shipped

Seventeen new database tables on top of a sixty-table multi-tenant base, all with row-level security; two purpose-built database functions — the vector search and the atomic minute deduction; forty-nine migrations applied to the live database.

A visual agent builder with a drag-and-drop call-flow canvas across seven node types, published version snapshots with rollback, and a per-agent knowledge base with document, DOCX, and URL ingestion into pgvector. Five swappable provider layers with a live connection test per provider and a mode selector between middleware and direct. An outbound campaign dialer with a real job queue, do-not-call checking before every dial, a full campaign state machine, and per-campaign analytics. Call logs with transcript, recording, disposition, and cost; a transcript player synced to the audio. Minute-credit billing across four database-driven plans, one-time minute packs, and a low-balance alert event. Contact management with phone-deduplicated bulk import, a do-not-call list, per-contact call history, and phone-number provisioning. An administrative plane verified isolated after the styling pass.

Two hundred and five routes compile; type-check and lint clean; every text string routed through the translation layer with 3,976 seeded rows and zero missing keys. A post-release automated check against the live deployment ran 26 deterministic assertions — HTTP, authenticated page renders, database state — and returned zero broken, zero blocked, zero errors.

## See it

[See Callia →](https://callia.saascode.ai)

## Related reading

- [Best AI Voice Agent Platforms in 2026, Ranked by Who Owns Your Phone Numbers When You Leave](https://saascode.ai/inside/best-ai-voice-agent-platforms-in-2026-ranked-by-who-owns.md)
- [What You Still Pay — Then What Separates the Platforms](https://saascode.ai/inside/callia-vs-dograh-ai-voice-agent-platform-comparison-self.md)
