Zoiper is a cross-platform softphone client that supports SIP and IAX protocols, enabling users to make voice and video calls over the internet.
It’s available for macOS, Windows, Linux, iOS, and Android.
Zoiper is compatible with most VoIP providers and PBX systems.
Key Features:
- Supports SIP and IAX protocols
- Works with audio and video calls
- Secure with TLS, SRTP, and ZRTP encryption
- Compatible with various codecs (G.729, Opus, G.711, etc.)
- Contact integration and click-to-dial support (with Chrome extension)
- Free and paid versions (Pro adds video, encryption, and more codecs)
Setup only involves entering SIP credentials provided by your VoIP or PBX provider. See SIP / VoiP

User Manual: https://www.zoiper.com/pdf/User%20Guide%20Zoiper%205%20v.1.0.7.pdf
24 Mar 2025
Started (re)using.
XML Contact Service
Zoiper can read from an XML file to populate the contact list.
So I will generate automatically an XML file from my contacts database, on a daily basis (or manually if needed).
The great thing too is that I can generate many XML versions, and use each one of them in Zoiper as a separate contact list.
So e.g. I can have:
- all my contacts
- VIP contacts
- In email sequences
- French contacts
- etc.
XML needs to follow this format:
https://www.zoiper.com/documentation/XML%20Contact%20Service%20in%20Zoiper%205%20PRO.pdf
<?xml version="1.0" encoding="utf-8"?>
<Contacts>
<Contact id="123">
<Name>
<First></First>
<Middle></Middle>
<Last></Last>
<Display></Display>
</Name>
<Info>
<Company></Company>
</Info>
<Phone>
<Type></Type>
<!--> Possible value one of the following: Work or Home -->
<Type></Type>
<!--> Possible value one the following: Phone, Cell, Pager, IPPhone, Mail, Fax, Pager or Custom -->
<CustomType></CustomType>
<!--> This is a custom text. It will be used only when “Custom” option is selected in <Type></Type> -->
<Phone></Phone>
<!--> The value is the actual phone number of the contact.
For the contact to show in zoiper5 at least one phone field should be present -->
<Account></Account>
<!--> The value is the ident value of the account that will be used for dialing, it is found in the config.xml file
If not provided Zoiper will use the XML contact service account (by default it’s the default account) -->
<Presence></Presence>
<!--> The ident value of the account that will be used for presence, it is found in the config.xml file
By default it’s do not use unless it is an IPPhone type -->
<AccountMappingType></AccountMappingType>
<!--> Possible value one of the following:
None = do not use
Default = the default account used by zoiper5
Service = the account used by XML contact service
Custom = when this is used, the ident value of the account should be provided in <Account></Account>, it is found in the config.xml -->
<PresenceMappingType>None/Service/Custom</PresenceMappingType>
<!--> Possible value one of the following:
None = do not use
Service = the account used by XML contact service.
Custom = when this is used, the ident value of the account should be provided in <Presence></Presence>, it is found in the config.xml -->
</Phone>
<!--> If required more than one phone for a contact a new phone tag should be added that includes at least the two types tags and phone tag -->
<Avatar>
<URL></URL>
</Avatar>
</Contact>
</Contacts>
Code has been optimised to be able to generate multiple XML files, and easily add new query/file pairs.
# Define the queries and their corresponding output files
QUERY_CONFIG = [
{
"query": """
SELECT rowid, domain, first, last, country, title, phone_mobile, phone_hq, phone, company
FROM people
WHERE connected IS NOT NULL
""",
"output_file": "/Users/xx/xx/xx/zoiper_contacts_all.xml"
},
{
"query": """
SELECT rowid, domain, first, last, country, title, phone_mobile, phone_hq, phone, company
FROM people
WHERE connected IS NOT NULL
AND nicai = 1
""",
"output_file": "/Users/xx/xx/xx/zoiper_contacts_nicai.xml"
}
]
# FUNCTIONS
def prettify(elem):
"""Return a pretty-printed XML string for the Element."""
rough_string = ET.tostring(elem, 'utf-8')
reparsed = minidom.parseString(rough_string)
return reparsed.toprettyxml(indent=" ")
def create_contacts_xml(cursor, query, output_file):
"""Generate XML file for contacts based on query results."""
cursor.execute(query)
rows = cursor.fetchall()
count_total = len(rows)
count = 0
count_row = 0
# Create the root element for the XML
root = ET.Element("Contacts")
# Process each row from the database
for row in rows:
count_row += 1
rowid, domain, first, last, country, title, phone_mobile, phone_hq, phone, company = row
if verbose:
print(f"Processing {first} {last} from {domain}")
# Skip if no phone number available
if not any([phone_mobile, phone_hq, phone]):
continue
# Create contact element with ID
contact = ET.SubElement(root, "Contact")
contact.set("id", str(rowid))
# Add name information
name = ET.SubElement(contact, "Name")
first_name = ET.SubElement(name, "First")
first_name.text = first
middle = ET.SubElement(name, "Middle")
last_name = ET.SubElement(name, "Last")
last_name.text = last
display = ET.SubElement(name, "Display")
display.text = f"{first} {last} ({title} @ {company})"
# Add company information
info = ET.SubElement(contact, "Info")
company_elem = ET.SubElement(info, "Company")
company_elem.text = domain
# Add phone numbers
for phone_value, phone_type in [
(phone_mobile, "Cell"),
(phone_hq, "Phone"),
(phone, "Phone")
]:
if phone_value and (phone_type != "Phone" or (phone_value != phone_mobile and phone_value != phone_hq)):
phone_elem = ET.SubElement(contact, "Phone")
type1 = ET.SubElement(phone_elem, "Type")
type2 = ET.SubElement(phone_elem, "Type")
type2.text = phone_type
phone_number = ET.SubElement(phone_elem, "Phone")
phone_number.text = phone_value
account_mapping = ET.SubElement(phone_elem, "AccountMappingType")
account_mapping.text = "Default"
presence_mapping = ET.SubElement(phone_elem, "PresenceMappingType")
presence_mapping.text = "None"
count += 1
# Write the XML to a file
with open(output_file, "w", encoding="utf-8") as f:
f.write(prettify(root))
print(f"\n✅ XML file generated: {output_file}")
return count_total, count_row, count
# MAIN
with sqlite3.connect(DB_BTOB) as conn:
cur = conn.cursor()
# Process each query configuration
for config in QUERY_CONFIG:
count_total, count_row, count = create_contacts_xml(cur, config["query"], config["output_file"])
print(f"\nStats for {config['output_file']}:")
print(f"Total records:\t{count_total:,}")
print(f"Processed rows:\t{count_row:,}")
print(f"Phone numbers:\t{count:,}")