Back to Systems

Call Intelligence Platform

A real-time operations platform connecting a PBX phone system with the company operations dashboard—live caller context and searchable conversation history for customer-care teams.

The Problem

Customer-care agents handled every conversation through a PBX softphone while operational work happened inside our internal Operations platform. The two systems had no awareness of each other.

When an incoming call arrived, agents had to manually search for the customer before they could help them. There was no centralized history of calls, no way to know who answered a conversation, and no searchable record once the call ended. Supervisors couldn’t review interactions, measure activity, or investigate customer issues without switching between multiple systems.

The goal wasn’t simply to log phone calls—it was to make telephony part of the operational workflow.

Constraints

The PBX ecosystem offered limited integration capabilities and exposed different APIs depending on the component being used. One challenge was establishing a trustworthy identity between phone extensions and operations users without relying on assumptions or manually maintained mappings.

The system also needed to satisfy two different requirements:

  • Real-time notifications while calls were happening.
  • Durable, searchable records after the conversation finished.

Those requirements ultimately led to two generations of the architecture rather than continuously extending the original design.

Solution Architecture

Generation One — Real-Time Call Awareness

The first version focused on helping agents during live conversations.

A custom Wave plugin emitted webhook events whenever calls were initiated, answered, or ended. Rather than trusting shared identifiers, I designed a token-based identity system that allowed each agent to securely bind their softphone to their Operations account.

Incoming events were processed through Laravel and broadcast using Laravel Reverb over dedicated WebSocket channels. When a customer called, the assigned agent immediately received a notification inside the Operations platform.

Instead of opening a generic customer profile, notifications deep-linked directly to the customer’s latest order after interviewing customer-care agents and learning that most incoming calls concerned recent purchases.

Generation Two — Durable Call Journals

As requirements evolved, the business needed more than live notifications—they needed a permanent operational record.

An n8n pipeline collected Call Detail Records (CDRs), enriched them with AI-generated transcripts, summaries, sentiment analysis, and disposition metadata, then delivered the payload to a Laravel webhook secured with custom X-Secret authentication.

The ingestion pipeline was intentionally idempotent. Every journal entry was keyed by the PBX CDR identifier, allowing duplicate deliveries without duplicate records.

Agent attribution was resolved automatically using PBX extension numbers:

  • Outbound calls matched the source extension.
  • Incoming calls matched the destination extension.
  • Unknown extensions remained explicitly unassigned rather than guessing ownership.

The resulting journal became fully searchable by phone number, customer name, transcript contents, agent, disposition, sentiment, and date.

Technologies

  • Backend: Laravel, PHP
  • Database: MySQL
  • Real-time: Laravel Reverb, WebSockets
  • Admin & UI: Livewire, Filament
  • Auth: Custom X-Secret authentication
  • Integration: n8n, Grandstream Wave
  • Testing: Pest (feature tests), PHPUnit (unit tests)

Engineering Decisions

Several implementation decisions proved more important than the individual features themselves.

Identity before convenience

Rather than assuming phone extensions belonged to specific users, I introduced a token-based binding process that explicitly connected a Wave client to an Operations account. This removed impersonation risks while keeping onboarding simple.

Idempotent ingestion

External systems retry requests. Instead of treating retries as failures, journal creation was designed to be idempotent so duplicate deliveries became harmless.

Separate real-time from persistence

Live notifications and permanent storage solved different problems. Treating them as separate systems made both architectures simpler and easier to evolve.

Store raw payloads

Alongside normalized data, every webhook payload was preserved. This made replaying events and debugging integration issues significantly easier without depending on the PBX.

What I Learned

This project fundamentally changed how I think about integrations.

The difficult part wasn’t receiving webhook requests—it was designing reliable boundaries between systems that I didn’t control.

I learned that identity, idempotency, and replayability matter far more than the transport itself. Once those foundations are correct, retries become safe, failures become recoverable, and future requirements—such as AI-generated transcripts and sentiment analysis—can be added without redesigning the pipeline.

Perhaps the biggest lesson was recognizing when software has outgrown its original architecture. Instead of continuously layering features onto the real-time implementation, I redesigned the platform around durable event ingestion. Building the system twice ultimately produced a cleaner, more maintainable solution than trying to preserve the original architecture forever.