Smart Missed Calls is live: the missed-call list that cleans itself
We're launching Smart Missed Calls — the only module that distinguishes between a call missed on one extension (trivial) and a call NO ONE on the team picked up (the real problem). The list auto-clears when someone calls back or the caller returns. For FreePBX and VitalPBX.
TL;DR
Smart Missed Calls is now live on the PBXTools portal. The module solves two problems the standard CDR report on FreePBX/VitalPBX consistently misses:
- “Missed on one extension” ≠ “missed by the whole team”. If the call was answered by anyone in the queue/IVR/ring group, it doesn’t appear on the list.
- The list cleans itself. When a colleague calls back or the caller returns and someone answers, the entry transitions to closed automatically, with reason and audit trail.
Works on FreePBX (14-17) and VitalPBX (4.x multi-tenant). Three install steps. No dialplan changes.
The problem with the “missed calls” report nobody talks about
Log into FreePBX → Reports → CDR Reports. Filter by disposition = NO ANSWER. You get 47 rows. You screenshot it to the team chat with “guys, are we even picking up the phone?”.
The data is correct. The conclusion is wrong.
A call routed through the “Sales” queue:
- Rings ext 101 for 8s →
NO ANSWER(row 1) - Rings ext 102 for 8s →
NO ANSWER(row 2) - Rings ext 103 for 8s →
NO ANSWER(row 3) - Rings ext 104 for 8s →
NO ANSWER(row 4) - Maria on ext 105 picks up →
ANSWERED(row 5)
The report shows 4 “missed” rows. In reality, the call was served. No customer lost, no callback needed. Maria spoke for 4 minutes, closed the order, ticked the ticket.
If you look at the first 4 rows and act on them, you do two bad things: (1) you create a complaint meeting for a problem that doesn’t exist, (2) you distract ext 101-104 from real calls coming in right now.
How Smart Missed Calls sees the same call
The actual algorithm, simplified to its essence. For each inbound call, our agent runs a query on the PBX that aggregates all CDR rows sharing the same linkedid (= all events of one logical call) and verifies simultaneously:
-- Condition 1: NO operator definitively answered
SUM(disposition = 'ANSWERED'
AND lastapp = 'Dial'
AND dst REGEXP '^[0-9]+$'
AND CAST(dst AS UNSIGNED) BETWEEN 10 AND 999) = 0
-- Condition 2: there's evidence of unanswered ringing
AND (
SUM(disposition = 'NO ANSWER') >= 1
OR SUM(lastapp = 'missed_calls') >= 1 -- FreePBX native marker
)
For the example above, condition 1 FAILS (Maria answered, ext 105 between 10 and 999). The call is not missed. Period. It doesn’t appear in Smart Missed Calls.
For another call that rang at 4 extensions and nobody picked up — condition 1 passes (zero ANSWERED), condition 2 passes (4 NO ANSWER rows). That’s a real miss.
Aggregation: the same insistent caller appears once
Another critical detail. Andrew calls 6 times in 10 minutes, nobody catches it. The raw report gives you 6 entries “Andrew called and got no answer”. You actually want to see: “Andrew is calling insistently, prioritize”, not search through 6 identical rows.
Smart Missed Calls aggregates automatically by the key (client, caller_number, did):
| Field | Value |
|---|---|
caller_number | +40 745 123 456 |
caller_name | Andrew Pope (from CRM) |
did | 0376443322 |
route_label | Sales Queue |
extensions | ["101", "102", "103", "104"] |
missed_count | 6 |
first_missed_at | 14:02:11 |
last_missed_at | 14:11:48 |
status | open |
One entry. With clear history. With a big “×6” badge next to the name. Manager sees the priority instantly.
The list cleans itself — three automatic paths
This is the part I, honestly, haven’t seen implemented in any other CDR-based system I’ve evaluated. The dashboard list is dynamic. Entries disappear ON THEIR OWN when the problem is resolved, no checkbox required.
Path 1: cleared_by=returned
Maria, on ext. 105, dials Andrew’s number. The outbound call goes through, Andrew answers, they talk for 8 minutes. On the next CDR cycle (within 5 minutes max), the agent detects the row:
src=105, dst=+40745123456, disposition=ANSWERED, lastapp=Dial
It recognizes that +40745123456 is a caller_number with an open entry for this client, and marks the entry:
status=cleared
cleared_by=returned
cleared_by_extension=105
cleared_call_at=14:23:09
Audit log: action=cleared_returned, actor_type=agent, note="Returned by extension 105".
The entry leaves the “open” list automatically. Maria did no special clicking.
Path 2: cleared_by=answered_later
Or Andrew, frustrated, dials again 30 minutes later. This time someone answers (anyone, anywhere in queue/IVR). Inbound ANSWERED with the same caller_number and DID. Entry closes:
status=cleared
cleared_by=answered_later
cleared_by_extension=<extension that answered>
Audit log: cleared_answered. Disappears from the list on its own.
Path 3: cleared_by=manual
The case where someone handled the customer outside the phone — WhatsApp, email, in-person visit. For that there’s the “Mark Resolved” button in Filament (visible only when admin enabled smart_missed_calls_allow_manual_resolve for the client).
The permitted user clicks, optionally adds a note, confirms. The entry:
status=cleared
cleared_by=manual
cleared_by_user_id=<the user's id>
cleared_note="Called him on WhatsApp, took the order"
Full audit: cleared_manual with actor_type=user and actor_user_id.
Plus: retention and reopen
cleared_by=expired— an hourly cron auto-closes entries older than N days (per-client setting). No retention = “forever” mode, entries stay open until truly closed.reopened— if the caller dials again AFTER the entry was closed, the system reopens it automatically and incrementsmissed_count. Audit showsreopenedwithactor_type=agent.
Audit log — for when blind trust isn’t enough
One of the features managers use most. The smart_missed_calls_audit table is append-only. Every event writes a row with:
action(created / incremented / notified / cleared_* / reopened)actor_type(agent / user / cron)actor_user_id(when applicable)note(optional, for manual)metadata(JSON — extension that called back, time, etc.)
This lets you, as admin, verify whether an agent is marking entries “manually resolved” without an actual callback. You’ll see: cleared_manual by user 12 with no subsequent cleared_returned audit. Clear signal for a constructive conversation.
And if you suspect a manual resolve was forced, there’s the admin-only reopen action that brings the entry back and preserves the prior closure history in audit.
Notifications — 5 cadences + dedupe
Five ways to be alerted, per-client configurable:
- off — nothing, dashboard only
- realtime — email per new entry (with
(caller, did)dedupe, 5 min default, plus 50/h hard rate limit so the inbox doesn’t drown) - hourly — one email at the top of each hour with the past hour’s list
- daily — at
notify_daily_at(default 08:00) with yesterday’s report - weekly — on
notify_weekly_dayatnotify_daily_at - monthly — on
notify_monthly_dayatnotify_daily_at
Plus: unlimited recipients per portal, optional “use extension email” (sends to the user-of-the-extension’s email, not a generic inbox).
Privacy and non-interference
Two technical promises that matter:
1. Your data stays with you. Only metadata leaves to the portal: caller_number, caller_name (when present in CDR), did, route_label, extensions[], missed_count, first/last_missed_at, status. Never audio, never recordings, never conversation content.
2. We don’t touch anything in FreePBX/VitalPBX. No dialplan changes. No new AMI. No open ports. No deletion of native recording files. We read asteriskcdrdb (FreePBX) or per-tenant DBs (VitalPBX) read-only. UCP, Sonata, Call Reports native — all keep working as you know them. We are strictly observers.
Communication with the PBXTools portal goes through a WebSocket over HTTPS on port 443. No port forwarding, no VPN. Works through any corporate firewall.
Enterprise customers can opt for self-hosted deployment of the entire portal in their own VPC. In that case not even metadata leaves the customer’s infrastructure.
Compatibility at launch
| System | Versions | Status |
|---|---|---|
| FreePBX | 14, 15, 16, 17 | ✅ Full support, dual fallback (lastapp='missed_calls' + CDR aggregation) |
| VitalPBX | 4.x multi-tenant | ✅ Full support, strict per-tenant isolation |
| FusionPBX | — | 🛣️ Q2 2026 |
| Issabel | — | 🛣️ Q2 2026 |
How to activate
# 1. Free account on portal + add the PBX
https://portal.pbxtools.ro/register
# 2. On the PBX, install command (one line):
curl -fsSL https://portal.pbxtools.ro/agent/install \
| bash -s -- --api-key <YOUR_KEY>
# 3. In the portal: enable the module on the Client
# - smart_missed_calls_enabled = true
# - retention_days (or leave null = forever)
# - allow_manual_resolve (recommended: true)
# - notify_frequency, notify_daily_at, recipients
On the next keepalive cycle (5 minutes) you receive a backfill of calls from the past 24 hours.
Roadmap
- FusionPBX + Issabel — Q2 2026
- Push mode sub-30s via AMI listening (Enterprise)
- Mobile push for VIP callers (CRM-flagged)
- Voicemail-to-text integrated into the digest
Activate Smart Missed Calls. For technical questions — contact page or [email protected].