- Asterisk Phonebook: up to a couple hundred numbers, stored in AstDB, the number must match character for character.
- CallerID Lookup Sources with a MySQL source: the main option for an office, thousands of records, your own table, cached in AstDB.
- CID Superfecta: a chain of several sources, spam filtering, notifications. Requires module version 17.0.7 or newer, older versions have an RCE.
- func_odbc plus the Set CallerID module: full control, SQL escaping, minimal dependency on modules.
For 90% of offices, a separate MariaDB table with numbers normalized to 10 digits, plus a MySQL source in CallerID Lookup Sources, is enough. Below is how to set this up in 30-40 minutes.
Diagnosis: you set up FreePBX, and the phone only shows a number
FreePBX CallerID shows exactly what the provider sends by default. In most countries outside the US, that’s almost always a bare number: 79161234567, not a single letter. Sound familiar?
A client calls. A new sales rep picks up. He has no idea this is your biggest customer, who’s been waiting three days for an invoice. The CDR shows the same bare digits later, so you end up building a customer report by hand.
Your receptionist knew forty key client numbers by heart. The receptionist went on maternity leave. The directory went with her.
Here’s the root cause. In the US, carriers pass CNAM, the subscriber’s name, right in the signaling. In most other countries that service basically doesn’t exist. So the PBX has to look up the name from its own database. FreePBX can do this, in four different ways. Each one has its own pitfalls.
What you’ll get out of this
- A clear picture of which CallerID database option fits your size and needs.
- A working MariaDB table that matches a number in any format: +7, 8, 7, or 10 digits.
- Cyrillic (or any non-Latin script) names on phones and in CDR without garbled text.
- CSV contact import that you can schedule.
- Verification, troubleshooting, backup, monitoring, and a safe upgrade path.
- Ready-made alternatives: Phonebook, Superfecta, HTTP API, and func_odbc.
- An FAQ covering the questions people usually search for.
Time: 30-40 minutes for the main recipe. Add 20 minutes if you set up auto-sync and monitoring.
What you need: SSH access to the FreePBX server as root or via sudo, access to the admin web interface, a CSV with numbers and names. If you don’t have a CSV yet, we’ll make a two-line test file.
System requirements
| Component | Version | Comment |
|---|---|---|
| OS | Debian 12 Bookworm | The only officially supported OS for FreePBX 17 |
| FreePBX | 17.x | The recipe also works on 16.x, differences are noted in the text |
| Asterisk | 22 LTS by default, 20 and 21 also supported | Starting from 19, Asterisk has no app_mysql |
| Database | MariaDB 10.11 from the Debian 12 repository | Same one FreePBX itself uses |
| cidlookup module | 15.0.25+ / 16.0.12+ / any 17.x | From these versions on, the MySQL query goes through AGI instead of app_mysql |
| superfecta module | 16.0.40+ / 17.0.7+ | Only if you choose Superfecta. Below these versions: CVE-2026-54710 |
| Python | 3.11 from Debian 12 | For the import script and the HTTP API |
At the time of writing, FreePBX 17 with Asterisk 22 LTS is current by default. Before installing, check the latest releases on the FreePBX 17 page in Sangoma’s knowledge base and in the FreePBX community.
What’s covered in this article
- Why the name doesn’t show up even after you configure it.
- A comparison of CallerID database options and their limits.
- The recipe: a MariaDB table, import, MySQL source, binding it to an inbound route.
- Verification and troubleshooting, with commands.
- Alternatives: Phonebook, Superfecta, HTTP, func_odbc.
- Prevention, security, backup, upgrades.
- FAQ.
Root causes: why the caller name doesn’t show up in FreePBX
Before building a database, understand what’s actually broken. Half the forum questions boil down to “I set up CallerID Lookup, it doesn’t work.” It’s broken for one of the seven reasons below. All of them are fixable, each in a different way.
| Cause | Why it breaks things |
|---|---|
| The provider only sends the number | CNAM is essentially absent in most non-US telephony. There’s simply no name to pull from until the PBX looks one up. |
| The number arrives in a different format | The database has 8 916 123-45-67, the provider sends 79161234567 or +79161234567. An exact match finds nothing. |
| The source isn’t bound to the inbound route | A Lookup Source exists, but the Other tab on the Inbound Route has None selected. FreePBX never makes the query. |
| Default latin1 encoding | The Character Set field in the source is blank, so latin1 is used. Non-Latin text turns into question marks. |
| Stale cache in AstDB | With caching on, a name found once is always returned, even after you change it in the database long ago. |
| Outdated cidlookup module on a new Asterisk | Old versions of the module queried MySQL via app_mysql. It was removed in Asterisk 19, so the query silently fails. |
| The phone can’t display the name | Older handsets truncate names to 12-16 characters or don’t understand UTF-8. The name is in the CDR, not on the screen. |
Here’s the key part. Causes 2 and 4 account for 80% of the problems on real-world PBXs. Number format and encoding. That’s why the recipe below is built around exactly these two things: normalize the number inside the database itself, and set the encoding explicitly.
On cause 6: it’s documented in detail in ticket FREEPBX-23206. The module relied on app_mysql, and once it was removed in Asterisk 19, MySQL sources stopped working. Later the query was rewritten as an AGI script. If you’re on FreePBX 17 with updated modules, this isn’t a problem. If the server hasn’t been updated since 2021, it is.
CallerID database options in FreePBX: comparison and limits
Now to the point. FreePBX doesn’t have one single “CallerID database”. It has several mechanisms that solve the problem at different depths. Pick one based on volume, data source, and who’s going to maintain this database.
| Option | Storage | Reasonable size | Number format | Main limitation |
|---|---|---|---|---|
| Asterisk Phonebook | AstDB, cidname family | up to 200-300 records | Exact match only | No normalization, awkward bulk editing |
| CallerID Lookup: Internal | AstDB via Phonebook | same as Phonebook | Exact match | Essentially the same as Phonebook |
| CallerID Lookup: MySQL | Any MySQL/MariaDB database | tens of thousands and up | Any logic in SQL | A single query, a single column, latin1 encoding by default |
| CallerID Lookup: HTTP/HTTPS | Your web service | no limit | Determined by the service | Service latency adds to every inbound call |
| CallerID Lookup: ENUM | DNS zones from enum.conf | – | E.164 | Rarely used for names in most regions |
| CallerID Lookup: OpenCNAM | External service | – | US numbers | Useless for most non-US numbers, free tier is 60 requests/hour |
| CID Superfecta | Multiple chained sources | no limit | Rules and regex | More complex to set up, history of vulnerabilities |
| func_odbc + Set CallerID | Any database via ODBC | no limit | Any logic in SQL | Manual configs outside the GUI |
cidlookup source types are documented in detail in the official CallerID Lookup Sources guide. It also includes a warning most people skip: the name lookup can slow down the PBX. The query runs synchronously while the call sits on the inbound route.
Here’s a quick decision diagram if you don’t want to think too long.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Need a caller name database"] --> B{"More than 300 numbers?"}
B -->|No| C["Asterisk Phonebook"]
B -->|Yes| D{"Data already in a CRM with an API?"}
D -->|Yes| E["Lookup Source HTTP"]
D -->|No| F{"Need spam filtering and notifications?"}
F -->|Yes| G["CID Superfecta 17.0.7+"]
F -->|No| H{"Need full SQL flexibility?"}
H -->|Yes| I["func_odbc and Set CallerID"]
H -->|No| J["Lookup Source MySQL"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style F fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style H fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style C fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style E fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style I fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style J fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
Why the MySQL source is the main recipe here. It’s native, configured in the GUI, survives FreePBX upgrades, and gets included in the standard configuration backup. You control the table: you can populate it from a CRM, an ERP, Active Directory, or by hand. Superfecta is more powerful, but for the task of “show the client’s name,” it’s overkill, like using a forklift to carry a grocery bag.
Solution architecture
Here’s how a call flows after setup. The provider sends a number, the inbound route calls the Lookup Source, which checks the cache, then queries MariaDB. The name found gets written to CALLERID(name), which phones and the CDR pick up.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Inbound call from provider"] --> B["Inbound Route by DID"]
B --> C["CID Lookup Source"]
C --> D{"Number already cached in AstDB?"}
D -->|Yes| E["Name from cache"]
D -->|No| F["AGI cidlookup_mysql"]
F --> G["MariaDB cid_directory"]
G --> H["CALLERID name"]
E --> H
H --> I["Ring Group or IVR"]
I --> J["Phone screen"]
H --> K["CDR cnam field"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style J fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
Port reference
| Port | Protocol | Purpose | Exposed externally? |
|---|---|---|---|
| 5060 | UDP/TCP | SIP, PJSIP | Only to the provider’s IP and office networks |
| 5061 | TCP | SIP over TLS | Only if you use TLS |
| 10000-20000 | UDP | RTP, voice | Yes, for the provider and remote extensions |
| 80/443 | TCP | FreePBX web interface | No, admin network only |
| 3306 | TCP | MariaDB | No. Listens only on 127.0.0.1 |
| 8085 | TCP | HTTP API for CallerID, if you choose that option | No. Only from the PBX’s IP |
Recipe: a CallerID database for FreePBX on MariaDB
Let’s agree on names so nothing gets mixed up. The FreePBX server is called pbx01. The database lives on the same MariaDB instance that FreePBX already uses, but in a separate database called cid_directory. If you want to move the database to a separate server, everything stays the same, the only difference is the Host field and your firewall rules.
Preparation: check your environment
First, confirm you’re on the versions discussed here. Five seconds now saves an hour later.
Host: pbx01, the FreePBX server, as root.
cat /etc/debian_version
asterisk -rx "core show version"
mariadb --version
fwconsole ma list | grep -Ei 'framework|core|cidlookup|superfecta|phonebook|setcid'
Result: Debian 12.x, Asterisk 20, 21 or 22, MariaDB 10.11. The module list shows framework and core at version 17.x. The cidlookup module may be missing, we’ll install it. If superfecta is installed below version 17.0.7, read the security section under alternatives first.
Before making any changes, take a snapshot. If FreePBX runs in a VM on Proxmox, it’s one command.
Host: the Proxmox node running the pbx01 VM. Replace 105 with your PBX’s VMID.
qm snapshot 105 pre-callerid-db --description "Before CallerID DB setup"
Result: the snapshot shows up in the Proxmox web interface under the Snapshots tab. Rollback: qm rollback 105 pre-callerid-db.
Step 1. Install or update the CallerID Lookup module
Why: in current module versions, the MySQL source works via an AGI script. In older ones, it used app_mysql, which doesn’t exist in Asterisk 22. Updating fixes cause 6 from the table above.
Host: pbx01, as root.
fwconsole ma downloadinstall cidlookup
fwconsole ma upgrade cidlookup
fwconsole reload
ls -l /var/lib/asterisk/agi-bin/ | grep -i cidlookup
Result: the module shows Enabled, and the agi-bin directory has a file named cidlookup_mysql.agi or similar. If the file is missing but the module is current, check whether the AGI lives inside the module’s own folder: find /var/www/html/admin/modules/cidlookup -name “*.agi”.
Step 2. Create the database and two users
Why: a separate database doesn’t collide with FreePBX’s own tables and survives FreePBX upgrades. Two users isn’t paranoia here. FreePBX inserts the caller’s number directly into the SQL query text. So the user doing the lookup should only have SELECT, and only on one table. A second user will handle writes for imports.
Host: pbx01, as root. On Debian 12, root connects to MariaDB via unix_socket without a password.
mariadb
Inside the MariaDB client, run this. Replace the passwords with your own, long and random. You can generate one with openssl rand -base64 24 in another window.
CREATE DATABASE cid_directory CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'cid_ro'@'localhost' IDENTIFIED BY 'REPLACE_WITH_LONG_PASSWORD_RO';
CREATE USER 'cid_rw'@'localhost' IDENTIFIED BY 'REPLACE_WITH_LONG_PASSWORD_RW';
GRANT SELECT ON cid_directory.* TO 'cid_ro'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON cid_directory.* TO 'cid_rw'@'localhost';
FLUSH PRIVILEGES;
Result: each command returns Query OK. Check the privileges with: SHOW GRANTS FOR ‘cid_ro’@’localhost’;, the output should show only SELECT.
Step 3. Create the table with number normalization
This is the key step in the whole article. Don’t skip it.
The problem: in one database, numbers are stored as “+7 (916) 123-45-67”, “89161234567”, and “7 916 1234567”. The provider sends “79161234567”. Tomorrow you switch providers, and the new one sends “+79161234567”. Comparing strings as-is gets you nowhere.
The fix: store a computed column next to the original number, the last 10 digits. For mobile and landline numbers in most countries this is the unique part. The column is STORED, with a unique index on it. MariaDB recalculates it automatically on every insert and update.
Host: pbx01, same MariaDB client.
USE cid_directory;
CREATE TABLE contacts (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
number VARCHAR(32) NOT NULL,
num10 VARCHAR(10) AS (RIGHT(REGEXP_REPLACE(number, '[^0-9]', ''), 10)) STORED,
name VARCHAR(64) NOT NULL,
source VARCHAR(32) NOT NULL DEFAULT 'manual',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_num10 (num10)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO contacts (number, name) VALUES
('+7 (916) 123-45-67', 'John Smith Acme Corp'),
('8 (343) 222-33-44', 'Acme Corp Accounting');
SELECT number, num10, name FROM contacts;
Result: SELECT returns two rows, with num10 showing 9161234567 and 3432223344. Parentheses, spaces, plus signs, and leading digits are gone.
Why 10 digits and not 11. The leading digit on some numbering plans varies (7 or 8, or might be missing entirely). The last 10 digits stay stable across all these notations. If you work with international numbers where the country matters, store the full E.164 and compare on that instead. But for domestic traffic, 10 digits get the job done.
On name length: VARCHAR(64) gives you headroom, but phones will show less. Most desk phones display 16-24 characters on the incoming call line. So “John Smith Acme Corp” beats “Acme Corporation Limited, John Smith, Chief Engineer”.
Step 4. Import contacts from a CSV
A phone directory born in Excel doesn’t fear normalization. It’s just never heard of it. That’s why we do the import with a script, not by hand.
CSV format: two fields separated by a semicolon, number and name, UTF-8 encoding. Excel can save as “CSV UTF-8”, pick exactly that option. Example file:
+7 (916) 123-45-67;John Smith Acme Corp
8 343 222-33-44;Acme Corp Accounting
79035556677;Jane Doe Vector LLC
First install the MariaDB driver for Python and store the cid_rw credentials in a file readable only by root. We won’t hardcode the password in the script.
Host: pbx01, as root.
apt update && apt install -y python3-pymysql
cat > /root/.cid_rw.cnf <<'EOF'
[client]
user=cid_rw
password=REPLACE_WITH_LONG_PASSWORD_RW
host=localhost
EOF
chmod 600 /root/.cid_rw.cnf
Now the script itself. It reads the CSV, skips empty lines and numbers shorter than 10 digits, truncates the name to 64 characters, and does an upsert: new numbers get inserted, existing ones get updated.
Host: pbx01, file /usr/local/sbin/cid-import.py.
#!/usr/bin/env python3
"""Import CallerID contacts from CSV into cid_directory.contacts.
Line format: number;name (UTF-8, BOM allowed)."""
import csv
import sys
import pathlib
import pymysql
CNF = "/root/.cid_rw.cnf"
STAMP = pathlib.Path("/var/lib/cid-sync/last_ok")
SQL = (
"INSERT INTO contacts (number, name, source) VALUES (%s, %s, 'csv') "
"ON DUPLICATE KEY UPDATE number = VALUES(number), "
"name = VALUES(name), source = 'csv'"
)
def load(path):
rows, skipped = [], 0
with open(path, newline="", encoding="utf-8-sig") as f:
for rec in csv.reader(f, delimiter=";"):
if len(rec) < 2:
skipped += 1
continue
number, name = rec[0].strip(), rec[1].strip()[:64]
digits = "".join(c for c in number if c.isdigit())
if len(digits) < 10 or not name:
skipped += 1
continue
rows.append((number, name))
return rows, skipped
def main():
if len(sys.argv) != 2:
sys.exit("usage: cid-import.py contacts.csv")
rows, skipped = load(sys.argv[1])
conn = pymysql.connect(read_default_file=CNF, database="cid_directory",
charset="utf8mb4")
with conn:
with conn.cursor() as cur:
cur.executemany(SQL, rows)
conn.commit()
STAMP.parent.mkdir(parents=True, exist_ok=True)
STAMP.touch()
print(f"ok: {len(rows)} upserted, {skipped} skipped")
if __name__ == "__main__":
main()
Host: pbx01, as root. Make the script executable and run it against the file. Copy the file to the server beforehand, for example to /srv/cid/contacts.csv.
chmod 750 /usr/local/sbin/cid-import.py
mkdir -p /srv/cid
/usr/local/sbin/cid-import.py /srv/cid/contacts.csv
Result: a line like ok: 3 upserted, 0 skipped. If skipped is greater than zero, open the CSV and check which lines have no name or a too-short number. Usually these are internal three-digit extensions, which don’t belong in this table anyway.
One subtlety worth noting. If the CSV has two different notations for the same number, the last one overwrites the first, since the unique index on num10 prevents a duplicate. That’s by design: one number, one name.
Step 5. Test the query by hand before touching the GUI
Why: if the query doesn’t work in the console, it won’t work in FreePBX either. And debugging it inside FreePBX takes longer. We’re testing exactly the user and exactly the query that will end up in the Lookup Source.
Host: pbx01. You’ll enter the cid_ro password interactively.
mariadb -u cid_ro -p --default-character-set=utf8mb4 cid_directory \
-e "SELECT name FROM contacts WHERE num10 = RIGHT('+79161234567', 10) LIMIT 1;"
mariadb -u cid_ro -p --default-character-set=utf8mb4 cid_directory \
-e "SELECT name FROM contacts WHERE num10 = RIGHT('89161234567', 10) LIMIT 1;"
Result: both queries return “John Smith Acme Corp” with the text rendering correctly. Two different formats, one name. That’s exactly what we were after.
Why RIGHT appears in the query itself, not only in the column definition. The number from the provider also arrives in an unpredictable shape. We normalize both sides of the comparison. The index on num10 is still used: the function is applied to the constant, not the column.
Step 6. Create a source in CallerID Lookup Sources
Why: this is the official hook point FreePBX uses to attach a name to an incoming call. Open the web interface: Admin → CallerID Lookup Sources → Add CID Lookup Source. Fill in the fields as follows:
| Field | Value | Comment |
|---|---|---|
| Source Description | CID MariaDB | This is the name you’ll see in the inbound route’s dropdown |
| Source type | MySQL | Works with MariaDB too |
| Cache Results | No, for now | You’ll turn it on later once the database settles. Details below |
| Host | localhost | Specifically localhost, not 127.0.0.1, since the user was created for localhost |
| Database | cid_directory | |
| Query | the query shown below | FreePBX replaces [NUMBER] with the caller’s number |
| Username | cid_ro | SELECT only |
| Password | cid_ro’s password | |
| Character Set | utf8mb4 | Don’t leave it blank, or you’ll get latin1 and garbled text |
The query for the Query field, as one line:
SELECT name FROM contacts WHERE num10 = RIGHT('[NUMBER]', 10) LIMIT 1
Click Submit, then Apply Config. The query should return exactly one column. FreePBX takes the first value of the first row and writes it to the caller’s name.
Step 7. Bind the source to an inbound route
Why: a Lookup Source by itself does nothing. It needs to be enabled on a specific Inbound Route. This is the most common cause of “I set everything up and nothing happens.”
Open Connectivity → Inbound Routes → the route you want → the Other tab. In the CID Lookup Source field, pick “CID MariaDB”. Save and apply config. If you have several inbound routes, repeat for each one where you want names resolved. Don’t forget the catch-all route with an empty DID.
After that, regenerate the dialplan from the console, it’s more reliable than the browser button and shows errors right away.
Host: pbx01, as root.
fwconsole reload
asterisk -rx "dialplan show cidlookup"
Result: the cidlookup context has an extension like cidlookup_1 calling the AGI. The number after the underscore is the source ID. If the context is empty, the source isn’t bound to any route, or the config wasn’t applied.
Here’s how this looks over time. Notice that while the query runs, the caller hasn’t heard ringing yet. So a fast query isn’t just nice to have, it’s a quality-of-service issue.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#f8fafc',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'noteBkgColor': '#fefce8',
'noteTextColor': '#713f12',
'noteBorderColor': '#fbbf24',
'actorBkg': '#f8fafc',
'actorBorder': '#94a3b8',
'actorTextColor': '#1e293b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'sequence': {
'mirrorActors': false,
'messageAlign': 'center',
'actorMargin': 120,
'width': 160,
'noteMargin': 12
}
}}%%
sequenceDiagram
participant P as Provider
participant A as Asterisk
participant G as AGI cidlookup
participant D as MariaDB
participant T as Phone
P->>A: INVITE with number
A->>A: Check AstDB cache
A->>G: Caller number
G->>D: SELECT name
D->>G: John Smith Acme Corp
G->>A: CALLERID name
Note over A,D: Delay before ringing
A->>T: Call with name
Step 8. Decide whether to enable caching
The Cache Results checkbox in the source makes FreePBX store the found name in AstDB. The next call from the same number skips the database query entirely. Fast. But there’s a catch.
A cached name is always returned, even if you changed it in the table. Even if the client left for a competitor and the number now belongs to someone else. The cache doesn’t expire on its own.
| Situation | Cache | Why |
|---|---|---|
| Database is local, on the same server | Off | An indexed query takes milliseconds, there’s no gain, and stale names are a real risk |
| Database on a remote server on the same network | Depends | Enable it if latency to the database is noticeable, and clear the cache after every sync |
| HTTP API backed by a slow external service | On | Otherwise every inbound call waits on the service’s response |
| Names change frequently | Off | Otherwise people see stale names and stop trusting the system |
For the recipe in this article, you don’t need caching. A local MariaDB with a unique index responds faster than you can finish reading this sentence.
Verification: confirming FreePBX CallerID shows the name
Check three places: the live Asterisk log, the CDR, and the phone’s screen. If the name shows up in the CDR but not on the handset, the problem is the phone. If it’s missing in the CDR too, the problem is the database or the route binding.
Live log during a test call
Host: pbx01, as root. Open the console and call a landline number from a mobile number that’s in the database.
asterisk -rvvvvv
Look for lines mentioning cidlookup in the output. There should be an AGI call followed by a Set with CALLERID(name). Exit the console with exit or Ctrl+C, Asterisk itself keeps running.
If the call already happened, check the log file instead:
grep -iE 'cidlookup|CALLERID\(name\)' /var/log/asterisk/full | tail -n 30
Result: you can see which number went into the query and which name came back. If the number in the log looks unexpected, say, with a route prefix or an extra plus sign, that’s your mismatch right there.
Checking via CDR
CDR stores the name in the cnam field. Handy, since it shows the most recent calls and what was found for each one.
Host: pbx01, in the mariadb client as root.
SELECT calldate, src, cnam, did, disposition
FROM asteriskcdrdb.cdr
WHERE calldate > NOW() - INTERVAL 1 HOUR
ORDER BY calldate DESC
LIMIT 10;
Result: the test call shows the name from your table in cnam. If it shows a number or is empty, the lookup didn’t fire. The src column also shows you exactly what format the provider sends.
Checking the AstDB cache
Even with caching disabled, it’s useful to know what’s sitting in AstDB. Both Phonebook and the cidlookup cache use the same cidname family.
asterisk -rx "database show cidname" | head -n 20
Result: a list of keys like /cidname/79161234567 with a name value. If there’s a stale name there for a given number, that’s exactly what gets shown, and your table never even gets queried.
Debugging FreePBX
If the AGI stays silent or returns nothing, turn on FreePBX’s debug log. Keep the window open and make a test call from a second window.
fwconsole dbug
The log goes to /var/log/asterisk/freepbx_dbug. It shows PHP-side database connection errors that never show up in the Asterisk console.
Troubleshooting: common mistakes and how to fix them
Usually this is the part where articles claim “if you followed the instructions exactly, there won’t be any problems.” There will be problems. A human wrote the instructions, and a human set up the server.
| Error | Cause | Fix |
|---|---|---|
| Garbled text instead of the name | Character Set is blank, latin1 is used | Set utf8mb4 in the source, verify the table’s encoding |
| Name doesn’t update after editing the database | Caching is on, the value is sitting in AstDB | Delete the key from cidname or disable caching |
| Lookup finds nothing, but works fine in the console | The provider sends the number in a different format | Check src in the CDR, verify normalization |
| Access denied for user | Source host is 127.0.0.1, but the user was created for localhost | Use localhost, or create a user for 127.0.0.1 |
| “unknown” shows up instead of the number when no name is found | Old bug in cidlookup 15.0.24 | Update the module to a current version |
| Warning about app_mysql, can’t select MySQL | Old cidlookup on Asterisk 19+ | Update the cidlookup module |
| Delay before ringing starts | Slow remote database or HTTP API | Index, local database, caching |
| Name shows in CDR, not on the phone | Phone doesn’t render UTF-8 or truncates text | Firmware, phone’s language setting, shorter names |
Garbled text instead of proper characters
Cause: the Character Set field in the source is blank. The module’s own interface honestly warns that latin1 is used by default. Non-Latin text from a utf8mb4 table doesn’t fit in latin1.
Fix: open the source, enter utf8mb4, save, apply config. Then verify the table itself is actually in utf8mb4, since if it was created without an explicit charset on an old server, it might have ended up in latin1.
Host: pbx01, mariadb client as root.
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'cid_directory';
ALTER TABLE cid_directory.contacts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
The ALTER is only needed if TABLE_COLLATION isn’t utf8mb4. If the data was already loaded with the wrong encoding, converting won’t fix it, you’ll need to re-import from the CSV.
Name is stale and won’t refresh
Cause: caching is enabled on the source. The name sits in AstDB and comes back before the query ever reaches the database.
Fix: delete the key for that specific number. Write the number in the exact format the provider sends it in, check the output of database show cidname to see.
Host: pbx01, as root.
asterisk -rx "database del cidname 79161234567"
asterisk -rx "database show cidname" > /root/cidname-backup-$(date +%F).txt
asterisk -rx "database deltree cidname"
Number not found, even though it’s in the database
Cause: format. Most often the provider sends a number with a prefix, or even with a plus sign and country code that your record doesn’t have. Or the opposite: the table stores 10 digits, but the provider sends 7 digits for a local landline with no area code. In that case RIGHT returns 7 digits, and the comparison fails.
Fix: take the real src value from the CDR and run it through the manual query from step 5. If it doesn’t match, check how many digits actually arrive. For local numbers without an area code, there’s a workaround: add the area code in the Query field via CONCAT. For example, for area code 343:
SELECT name FROM contacts WHERE num10 = RIGHT(CONCAT('343', '[NUMBER]'), 10) LIMIT 1
This is a workaround for a specific provider. First try asking the provider to send the number in full format, many will do this on request within a day.
Access denied for user ‘cid_ro’
Cause: in MariaDB, a user is defined as a pair of name plus host. A connection to localhost goes through a socket, a connection to 127.0.0.1 goes through TCP, and these can end up being different accounts.
Fix: set Host = localhost in the source. If you specifically need 127.0.0.1, create a second user.
Host: pbx01, mariadb client as root.
CREATE USER 'cid_ro'@'127.0.0.1' IDENTIFIED BY 'SAME_PASSWORD_RO';
GRANT SELECT ON cid_directory.* TO 'cid_ro'@'127.0.0.1';
FLUSH PRIVILEGES;
“unknown” shows up instead of the number
Cause: in 2022, cidlookup version 15.0.24 was released, which wiped the original CallerID whenever no name was found in the database. This was discussed in a FreePBX forum thread and fixed in 15.0.25 and 16.0.12.
Fix: update the module. On FreePBX 17 this shouldn’t be an issue, but if the server was restored from a backup of an older system, check the version.
Host: pbx01, as root.
fwconsole ma list | grep cidlookup
fwconsole ma upgrade cidlookup
fwconsole reload
Delay before ringing starts
Cause: the lookup runs before the call reaches the phones. If the query takes two seconds, the caller hears two seconds of silence. It’s worst with HTTP sources and remote databases over a VPN.
Fix: measure the query time. For a local database with an index, this should be single-digit milliseconds.
Host: pbx01.
time mariadb -u cid_ro -p cid_directory \
-e "SELECT name FROM contacts WHERE num10 = RIGHT('79161234567', 10) LIMIT 1;"
mariadb -u cid_ro -p cid_directory \
-e "EXPLAIN SELECT name FROM contacts WHERE num10 = RIGHT('79161234567', 10);"
In EXPLAIN, the key column should show uq_num10, and type should show const. If it shows ALL, the index isn’t being used, double-check step 3. For a remote database or HTTP, enable caching and look at your network.
Name shows in CDR, not on the phone
Cause: the phone itself. Older models don’t handle UTF-8 in the From header, some only show the number unless name display is explicitly enabled. Plus truncation based on screen width.
Fix: update the firmware, check the phone’s display language and an option called something like “Display Name” or “Caller ID Display”. If the Inbound Route has a CID name prefix set, like “Sales:”, keep in mind it eats into the screen space. Sometimes it’s better to drop the prefix than lose the client’s name.
Superfecta isn’t sending notifications
Cause: Superfecta’s “Send to” features, whether email, Telegram, or a URL, only fire when some source actually found a name. No name, no notification. This is documented behavior, not a bug.
Fix: for testing, add an Abandon Lookup source as the last one in the chain, that way a name is always “found,” and you can verify that sending works.
Alternatives: Phonebook, Superfecta, HTTP API, and func_odbc
The main recipe covers the typical case. But sometimes the database is small, or the data lives in a CRM, or you need logic that just doesn’t fit into a single Query field. Let’s go through each alternative with setup steps.
Asterisk Phonebook: when you have fewer than a couple hundred numbers
The simplest path. The Asterisk Phonebook module stores number-name pairs directly in AstDB, under the cidname family. In CallerID Lookup Sources, it’s plugged in as type Internal. No external database, no passwords.
The limits are strict. Only exact matches work, “79161234567” and “89161234567” are different numbers to it. Bulk editing is awkward. No change history. For a team of ten people with thirty regular clients, fine. For a sales department with five thousand contacts, no.
You can add records through the GUI, via CSV import from the module’s interface, or straight from the console. The CSV format expected by the import varied between module versions, so export from the GUI first and see which columns it expects.
Host: pbx01, as root. Example of adding a record directly to AstDB, using the exact format the provider sends.
asterisk -rx 'database put cidname 79161234567 "John Smith Acme Corp"'
asterisk -rx 'database get cidname 79161234567'
If the provider sends numbers in two formats, you’ll have to add two entries. This is exactly why Phonebook becomes unmanageable as the database grows.
CID Superfecta: when you need multiple sources
CID Superfecta is an orchestrator. It queries several sources in sequence or in parallel: your own MySQL, Contact Manager, Phonebook, external services. It can flag spam, show a warning text for suspicious numbers, and send notifications by email, Telegram, Pushover, or to an arbitrary URL. You can attach different schemas to different inbound routes.
Where this is worth it: a call center that needs to check a number in a CRM, then in a shared database, then flag spam. An office where management wants a Telegram alert for calls from VIP clients. If you just need to show a name from one table, Superfecta is overkill.
Details on the vulnerability are on the CVE-2026-54710 entry on OpenCVE. Checking and updating:
Host: pbx01, as root.
fwconsole ma list | grep -i superfecta
fwconsole ma upgrade superfecta
fwconsole reload
fwconsole ma list | grep -i superfecta
Result: version 17.0.7 or newer. If you don’t need the module:
fwconsole ma uninstall superfecta
fwconsole ma remove superfecta
fwconsole reload
Custom sources for Superfecta are written in PHP and placed in /var/www/html/admin/modules/superfecta/sources/. This is powerful, but every such file is code that runs under the web server’s user. Keep that in mind before pulling sources from GitHub without reading them first.
HTTP API: when data lives in a CRM
If contacts live in a CRM, an ERP, or an in-house system, pulling them into a separate table isn’t always the right call. The HTTP or HTTPS source type in CallerID Lookup makes a GET request, substituting the number for the [NUMBER] token in the query string, and takes the response body as the name.
Here’s a minimal Flask service that returns a name for a number. For this example it reads the same table, but you’d swap in a query against your CRM’s API instead.
Host: pbx01 or a separate application server, as root. Packages from the Debian 12 repository.
apt install -y python3-flask python3-pymysql gunicorn
useradd --system --no-create-home --shell /usr/sbin/nologin cidapi
install -d -o cidapi -g cidapi -m 750 /opt/cidapi
Credentials for the API go in a separate file, readable only by the service’s user. Use cid_ro, the service doesn’t need write access.
cat > /opt/cidapi/db.cnf <<'EOF'
[client]
user=cid_ro
password=REPLACE_WITH_LONG_PASSWORD_RO
host=localhost
EOF
chown cidapi:cidapi /opt/cidapi/db.cnf
chmod 600 /opt/cidapi/db.cnf
Host: same server, file /opt/cidapi/app.py.
import re
import pymysql
from flask import Flask, request, Response
app = Flask(__name__)
SQL = "SELECT name FROM contacts WHERE num10 = %s LIMIT 1"
def lookup(number):
digits = re.sub(r"\D", "", number or "")
if len(digits) < 10:
return ""
conn = pymysql.connect(read_default_file="/opt/cidapi/db.cnf",
database="cid_directory", charset="utf8mb4",
connect_timeout=1, read_timeout=1)
with conn:
with conn.cursor() as cur:
cur.execute(SQL, (digits[-10:],))
row = cur.fetchone()
return row[0] if row else ""
@app.get("/cid")
def cid():
name = lookup(request.args.get("number", ""))
return Response(name, mimetype="text/plain; charset=utf-8")
Here the number is passed as a query parameter, not concatenated into a string. This closes off SQL injection entirely, unlike the Query field in the GUI. One-second timeouts keep a hung database from holding up the call.
A systemd unit to keep the service running on its own. Listen on localhost only if the API runs on the same server.
Host: same server, file /etc/systemd/system/cidapi.service.
[Unit]
Description=CallerID lookup HTTP API
After=network-online.target mariadb.service
[Service]
User=cidapi
Group=cidapi
WorkingDirectory=/opt/cidapi
ExecStart=/usr/bin/gunicorn --workers 2 --bind 127.0.0.1:8085 app:app
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
chown cidapi:cidapi /opt/cidapi/app.py
systemctl daemon-reload
systemctl enable --now cidapi
curl -s "http://127.0.0.1:8085/cid?number=%2B79161234567"; echo
Result: curl prints “John Smith Acme Corp”. In CallerID Lookup Sources, create an HTTP-type source: Host 127.0.0.1, Port 8085, Path /cid, Query number=[NUMBER]. Bind it to a route the same way as in step 7.
If the API runs on a separate server, listen on the internal IP and restrict the firewall to allow only the PBX’s IP. For HTTP across an untrusted network, use HTTPS with a proper certificate instead.
func_odbc and Set CallerID: maximum control
An option for when a single Query field isn’t enough. ODBC is Asterisk’s native path to external databases, the exact one that replaced the removed app_mysql. FreePBX 17 already uses ODBC for writing CDR, so the MariaDB driver is already installed on the server.
Here’s the layout. Define a DSN, set up a res_odbc connection and a func_odbc function in Asterisk. Call the function from the Set CallerID module, which you assign as the inbound route’s destination before the ring group. This way nothing in the generated dialplan gets hand-edited, it’s all GUI plus three config files.
The main advantage is escaping. func_odbc has an SQL_ESC function, so the caller’s number goes into the query safely.
Host: pbx01, as root. First check the driver name and which custom files get included in the Asterisk configs.
odbcinst -q -d
grep -n include /etc/asterisk/res_odbc.conf /etc/asterisk/func_odbc.conf 2>/dev/null
fwconsole ma downloadinstall setcid
Add the DSN to /etc/odbc.ini. Take the Driver value from the odbcinst output, on FreePBX 17 it’s usually MySQL or MariaDB.
[cid-directory]
Description = CallerID directory
Driver = MySQL
Server = localhost
Database = cid_directory
Port = 3306
Socket = /run/mysqld/mysqld.sock
Option = 3
Charset = utf8mb4
Test the DSN before involving Asterisk, using the isql utility from unixODBC.
echo "SELECT name FROM contacts LIMIT 1;" | isql -v cid-directory cid_ro 'RO_PASSWORD'
The Asterisk connection. If res_odbc_custom.conf is included inside res_odbc.conf, write there. If not, add it directly to res_odbc.conf, but keep in mind this file can get overwritten on an upgrade.
[cid]
enabled => yes
dsn => cid-directory
username => cid_ro
password => REPLACE_WITH_LONG_PASSWORD_RO
pre-connect => yes
max_connections => 5
The func_odbc function, same deal, goes into a custom file if one is included.
[CIDNAME]
dsn=cid
readsql=SELECT name FROM contacts WHERE num10 = RIGHT('${SQL_ESC(${ARG1})}', 10) LIMIT 1
synopsis=CallerID name lookup in cid_directory
asterisk -rx "module reload res_odbc.so"
asterisk -rx "module reload func_odbc.so"
asterisk -rx "odbc show cid"
asterisk -rx "core show function ODBC_CIDNAME"
Result: odbc show cid shows the connection as Connected, and the function ODBC_CIDNAME is described. Now in Admin → Set CallerID, create a record: CallerID Name set to ${ODBC_CIDNAME(${CALLERID(num)})}, CallerID Number set to ${CALLERID(num)}, destination your ring group. In the inbound route, set the destination to this Set CallerID record, and leave CID Lookup Source as None.
One subtlety: if no name is found, the function returns an empty string, and the phone shows nothing instead of the number. Fix it with an IF expression in the name field: ${IF($[“${ODBC_CIDNAME(${CALLERID(num)})}” != “”]?${ODBC_CIDNAME(${CALLERID(num)})}:${CALLERID(name)})}. Looks intimidating, works reliably. The query does run twice, but with an index it’s unnoticeable.
func_odbc and res_odbc syntax is documented in detail in the Asterisk documentation.
Alternatives compared
| Criteria | Phonebook | Lookup MySQL | Lookup HTTP | Superfecta | func_odbc |
|---|---|---|---|---|---|
| Setup time | 5 minutes | 30 minutes | 1 hour | 1-2 hours | 1 hour |
| Number normalization | No | Yes, in SQL | Yes, in code | Yes, rules | Yes, in SQL |
| SQL injection protection | Not applicable | User permissions only | Parameterized in code | Depends on the source | SQL_ESC |
| Multiple sources | No | No | As many as you write | Yes | Via dialplan |
| Notifications | No | No | As many as you write | Yes | Via dialplan |
| GUI only | Yes | Yes | Plus a service | Yes | No |
Prevention: keeping the CallerID database from going stale
A name database is useful as long as it stays current. Six months without syncing and it starts lying, a year and nobody trusts it anymore. So syncing, monitoring, and backups aren’t optional extras.
Auto-sync with a systemd timer
A one-liner in crontab without a comment outlives the employee who wrote it, and ends up knowing more about the system than they ever did. So instead of crontab, use a systemd timer: it shows up in the unit list, logs to the journal, and doesn’t get lost when the server gets migrated.
Layout: a CRM or export script drops a fresh CSV into /srv/cid/contacts.csv, and a timer runs the import once an hour.
Host: pbx01, as root. File /etc/systemd/system/cid-sync.service.
[Unit]
Description=Import CallerID contacts from CSV
ConditionPathExists=/srv/cid/contacts.csv
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cid-import.py /srv/cid/contacts.csv
File /etc/systemd/system/cid-sync.timer.
[Unit]
Description=Hourly CallerID contacts import
[Timer]
OnCalendar=hourly
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now cid-sync.timer
systemctl list-timers cid-sync.timer
systemctl start cid-sync.service && journalctl -u cid-sync.service -n 5 --no-pager
Result: the timer appears in the list with a next-run time, and the journal shows an “ok” line with a record count. If you turned caching on in the source, add a second ExecStart line to the service that clears stale keys, otherwise the sync is pointless.
The import script only adds and updates. Contacts removed from the CRM stay in the table. If that matters, clean up records from source csv that haven’t been touched in a while once a week, or reload the whole table inside a transaction.
Monitoring
What to monitor: that the sync ran recently, that the table isn’t empty, that MariaDB is alive. The import script touches a marker file at /var/lib/cid-sync/last_ok on every successful run. The age of that file is the best indicator.
An example for the Zabbix agent. The marker’s age uses the standard key vfs.file.time[/var/lib/cid-sync/last_ok,modify], with a trigger if it’s older than three hours. Record count via UserParameter.
Host: pbx01, file /etc/zabbix/zabbix_agent2.d/cid.conf. Store the cid_ro credentials at /etc/zabbix/.cid_ro.cnf, owned by zabbix with mode 600.
UserParameter=cid.rows,mariadb --defaults-extra-file=/etc/zabbix/.cid_ro.cnf -Nse "SELECT COUNT(*) FROM cid_directory.contacts"
systemctl restart zabbix-agent2
zabbix_agent2 -t cid.rows
Trigger on cid.rows: a drop of more than 20% between runs. That usually means someone exported an empty or truncated file from the CRM.
Backups
Here’s the key part. FreePBX’s built-in Backup module saves its own configuration and its own databases. It doesn’t know about cid_directory and won’t back it up. After a restore, the source will still be configured, but the table will be gone.
| What to back up | How often | Where to keep it | How to restore |
|---|---|---|---|
| The cid_directory database | Daily | Locally for 30 days plus an off-site copy | mariadb cid_directory from the dump |
| FreePBX configuration | Daily, via the Backup module | Remote storage configured in the Backup module | Restore module |
| Scripts and units | On every change | Git, or alongside the dump | Copy the files back |
| AstDB cidname family | Weekly, if you use Phonebook | Alongside the dump | database put from the list |
Host: pbx01, as root. Script /usr/local/sbin/cid-backup.sh.
#!/bin/bash
set -euo pipefail
DIR=/var/backups/cid
mkdir -p "$DIR"
mysqldump --single-transaction --default-character-set=utf8mb4 cid_directory \
| gzip > "$DIR/cid_directory_$(date +%F).sql.gz"
asterisk -rx "database show cidname" > "$DIR/cidname_$(date +%F).txt"
find "$DIR" -type f -mtime +30 -delete
chmod 750 /usr/local/sbin/cid-backup.sh
/usr/local/sbin/cid-backup.sh && ls -lh /var/backups/cid/
Scheduling works the same way, via a systemd timer with OnCalendar=daily. Pull the /var/backups/cid directory to off-site storage with whatever tool you already use for everything else: rsync, borg, rclone.
Restoring. Test it at least once before you actually need it.
gunzip -c /var/backups/cid/cid_directory_2026-10-25.sql.gz | mariadb cid_directory
mariadb -e "SELECT COUNT(*) FROM cid_directory.contacts;"
Autostart
Everything the lookup depends on needs to come up on its own after a reboot. Check it with one command.
systemctl is-enabled mariadb asterisk cid-sync.timer
systemctl is-enabled cidapi 2>/dev/null || true
Result: enabled across the board. Asterisk on FreePBX 17 is usually started through the freepbx unit, so the status for asterisk itself might differ, check systemctl is-enabled freepbx.
Security
A CallerID database is a list of clients with phone numbers. Personal data, plain and simple. Treat it accordingly.
MariaDB should listen only on localhost. Check it:
Host: pbx01.
ss -ltnp | grep -E ':3306|:8085'
Result: address 127.0.0.1, not 0.0.0.0. If the database listens on all interfaces and you haven’t moved it to a separate server, fix bind-address in /etc/mysql/mariadb.conf.d/50-server.cnf and restart MariaDB.
Firewall. If you’re using FreePBX’s own Firewall module, work within it and don’t run UFW alongside it, two iptables rule managers don’t play nicely together. If there’s no module, here’s a minimal UFW setup. Replace 203.0.113.10 with the provider’s IP and 10.0.0.0/24 with your office network.
Host: pbx01, as root.
ufw default deny incoming
ufw default allow outgoing
ufw allow from 10.0.0.0/24 to any port 22 proto tcp
ufw allow from 10.0.0.0/24 to any port 443 proto tcp
ufw allow from 10.0.0.0/24 to any port 5060 proto udp
ufw allow from 203.0.113.10 to any port 5060 proto udp
ufw allow 10000:20000/udp
ufw enable
ufw status numbered
fail2ban. Check whether it’s installed and whether a jail exists for Asterisk.
systemctl is-active fail2ban
fail2ban-client status
If fail2ban isn’t installed and FreePBX isn’t handling brute-force attempts on its own, install it and enable the asterisk jail. The filter already ships with Debian, just point the logpath at /var/log/asterisk/full.
apt install -y fail2ban
cat > /etc/fail2ban/jail.d/asterisk.local <<'EOF'
[asterisk]
enabled = true
logpath = /var/log/asterisk/full
maxretry = 5
findtime = 600
bantime = 3600
EOF
systemctl restart fail2ban
fail2ban-client status asterisk
SSH hardening, in a separate file so you don’t touch the main config. Replace admin with your actual username.
Host: pbx01, file /etc/ssh/sshd_config.d/10-hardening.conf.
PermitRootLogin prohibit-password
PasswordAuthentication no
MaxAuthTries 3
AllowUsers admin
sshd -t && systemctl reload ssh
sshd -t checks the syntax before reloading. If there’s an error, the reload won’t happen, and you won’t lose access. Confirm key-based login works ahead of time.
Upgrading without surprises
FreePBX upgrades touch the cidlookup module, Asterisk, and PHP. Your table doesn’t depend on any of that, but the AGI script and the source’s behavior do.
What to check before upgrading:
- A fresh VM snapshot or a full FreePBX backup, plus a dump of cid_directory.
- The list of available updates: what’s actually going to change.
- Release notes for the cidlookup and superfecta modules, if they’re on the list.
- Timing: upgrade outside business hours, calls in progress during fwconsole reload can drop.
Host: pbx01, as root.
/usr/local/sbin/cid-backup.sh
fwconsole ma showupgrades
fwconsole ma upgradeall
fwconsole reload
fwconsole ma list | grep -Ei 'cidlookup|superfecta'
After upgrading, make a test call and check the CDR as described in the Verification section. To roll back, use the VM snapshot on Proxmox: qm rollback with the VM ID and snapshot name, run on the Proxmox node.
Looking ahead, the FreePBX community has mentioned January 1, 2027 as the target release date for FreePBX 18. Before a major upgrade, verify how the MySQL source behaves on the new version, app_mysql was removed once already without much warning.
FAQ: common questions about CallerID in FreePBX
Why doesn’t CallerID Lookup in FreePBX work after I set it up?
Nine times out of ten, the source isn’t selected on the inbound route’s Other tab, or the provider sends the number in a different format than the database. Check src in the CDR and run the query manually with that number under the source’s user. If the system is old and the cidlookup module hasn’t been updated, it might still be trying to use the app_mysql function, which no longer exists in Asterisk.
How do I verify the CallerID database is working correctly?
Make a test call from a number in the database and check the cnam field in the asteriskcdrdb.cdr table. At the same time, the asterisk -rvvvvv console shows the AGI cidlookup call and the name it resolved. If the name is in the CDR but not on the handset, the problem is the phone, not the database.
What do I do if I get garbled text instead of the name?
Set utf8mb4 in the Character Set field of the MySQL source and apply the config. The module defaults to latin1, and non-Latin text doesn’t survive that. Also verify the table itself was created in utf8mb4, and that the CSV was saved in UTF-8.
How is CID Superfecta different from CallerID Lookup Sources?
CallerID Lookup uses one source per route: one database, one HTTP service, or Phonebook. Superfecta queries a chain of sources, can flag spam, and sends notifications. For simply showing a name from one database, Lookup Sources is enough, Superfecta is for more complex logic. Keep it on version 17.0.7 or newer.
How do I clear the CallerID cache in FreePBX?
The cache lives in AstDB under the cidname family. Delete a specific number with the database del command via asterisk -rx. Deleting the entire family also wipes any entries from the Phonebook module, so export it to a file first.
Can I pull names from Active Directory?
The cidlookup module can’t talk to LDAP directly. A working approach is a script that exports users with a telephoneNumber or mobile attribute to CSV, then runs the import from this article on a timer. For internal calls between extensions, you don’t need any of this, names there come from the extension settings themselves.
How many numbers can a CallerID database handle?
Phonebook in AstDB stays comfortable up to a couple hundred records. A MariaDB table with a unique index easily handles hundreds of thousands of numbers, with lookup times staying in the millisecond range. The bottleneck isn’t volume, it’s network latency if the database is remote.
Does OpenCNAM work outside the US?
Not for practical purposes. OpenCNAM relies on the US CNAM system, and its free tier is limited to 60 cached lookups per hour. For numbers outside that system, you’ll need to maintain your own name database.
Outlook
What we did. Went through all the CallerID database options in FreePBX and their limits. Set up a separate MariaDB table that matches a number in any notation. Imported contacts with a script, connected a MySQL source with the right encoding, and bound it to the inbound route. Locked down permissions, set up syncing, monitoring, and a backup that FreePBX’s built-in module would never have done on its own.
What works now. A sales rep sees “John Smith Acme Corp” on the phone before they even pick up. In the CDR, the number has a name next to it, so a customer report is a single query away. And when your receptionist goes on leave, the directory stays right where it belongs: on the server.
Оставайтесь на связи
Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.
Подписаться на IT-Аптеку →


