Hey @Somo IT
Could you try this script.
import json
import sys
from urllib.parse import quote
import requests
# Change these values, or pass them through environment variables.
SERVER = "http://localhost"
USERNAME = "zach@example.com"
PASSWORD = "Admin123"
# Optional. Set to the source ItemID to select a specific folder.
# Leave as None to use the user's primary Contacts folder.
SOURCE_ID = None
# Optional. Set to a contact ID to retrieve one complete contact.
CONTACT_ID = None
CLIENT_ID = "python-contacts-example"
class SmarterMailApiError(Exception):
pass
def check_response(response):
"""Raise an exception for HTTP or SmarterMail API errors."""
try:
data = response.json()
except ValueError:
response.raise_for_status()
raise SmarterMailApiError(
f"Expected JSON response, received: {response.text[:500]}"
)
if not response.ok:
raise SmarterMailApiError(
f"HTTP {response.status_code}: {json.dumps(data, indent=2)}"
)
# Most SmarterMail API result objects contain Success/Message.
if data.get("success") is False:
raise SmarterMailApiError(
f"API error {data.get('resultCode')}: {data.get('message')}"
)
return data
def authenticate(session):
url = f"{SERVER}/api/v1/auth/authenticate-user"
payload = {
"username": USERNAME,
"password": PASSWORD,
"clientId": CLIENT_ID,
}
response = session.post(url, json=payload, timeout=30)
data = check_response(response)
access_token = data.get("accessToken")
if not access_token:
raise SmarterMailApiError(
f"Authentication succeeded but no accessToken was returned: {data}"
)
session.headers.update({
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
})
return data
def get_contact_sources(session):
url = f"{SERVER}/api/v1/contacts/sources"
response = session.get(url, timeout=30)
data = check_response(response)
# The current result model uses SharedLists.
sources = data.get("sharedLists")
if sources is None:
# Tolerate older/case-preserving JSON serializers.
sources = data.get("SharedLists", [])
return sources or []
def choose_source(sources):
"""Choose the requested source or the primary personal Contacts source."""
if SOURCE_ID:
for source in sources:
if source.get("itemID") == SOURCE_ID:
return source
if source.get("ItemID") == SOURCE_ID:
return source
raise SmarterMailApiError(
f"Could not find contact source with ItemID {SOURCE_ID}"
)
# Prefer the authenticated user's primary source.
for source in sources:
is_primary = source.get("isPrimary", source.get("IsPrimary", False))
if is_primary and not source.get("isSharedItem", False):
return source
# Fall back to the first personal source.
for source in sources:
if not source.get("isSharedItem", False):
return source
raise SmarterMailApiError("No personal contact source was returned.")
def get_contacts(session, source):
owner = source.get("ownerUsername", source.get("OwnerUsername", ""))
source_id = source.get("itemID", source.get("ItemID"))
# The GAL uses owner/source ID "gal".
if source_id == "gal":
owner = "gal"
payload = {
"Sources": [
{
"Owner": owner,
"Id": source_id,
"SourceName": source.get(
"displayName",
source.get("DisplayName", "")
),
}
],
"SearchParams": {
"Skip": 0,
"Take": 1000,
"Search": "",
"SortField": "displayas",
"SortDescending": False,
"Categories": [],
"ShowNonCategorized": True,
"GetImages": False,
"FilterFlags": {},
},
}
url = f"{SERVER}/api/v1/contacts/contacts-all"
response = session.post(url, json=payload, timeout=60)
return check_response(response)
def get_one_contact(session, source, contact_id):
owner = source.get("ownerUsername", source.get("OwnerUsername", ""))
source_id = source.get("itemID", source.get("ItemID"))
if source_id == "gal":
owner = "gal"
# Route order is: shareOwner/source owner, source ID, contact ID.
url = (
f"{SERVER}/api/v1/contacts/get/"
f"{quote(str(owner), safe='')}/"
f"{quote(str(source_id), safe='')}/"
f"{quote(str(contact_id), safe='')}"
)
response = session.get(url, timeout=30)
return check_response(response)
def main():
with requests.Session() as session:
auth_result = authenticate(session)
print("Authenticated successfully.")
sources = get_contact_sources(session)
print("\nAvailable contact sources:")
for source in sources:
print(json.dumps(source, indent=2))
source = choose_source(sources)
print("\nSelected source:")
print(json.dumps(source, indent=2))
if CONTACT_ID:
contact = get_one_contact(session, source, CONTACT_ID)
print("\nContact:")
print(json.dumps(contact, indent=2))
else:
contacts = get_contacts(session, source)
print("\nContacts:")
print(json.dumps(contacts, indent=2))
if __name__ == "__main__":
try:
main()
except (requests.RequestException, SmarterMailApiError) as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
Should return something like this.
Authenticated successfully.
Available contact sources:
{
"enabled": true,
"access": 8,
"displayName": "Contacts",
"ownerUsername": "zach",
"itemID": "8477aee4e86b422bb7d8e2aeeb774ccb",
"isPrimary": true,
"shareType": "AddressBook",
"folderId": 460002,
"isSharedItem": false,
"isDomainResource": false,
"sharedResourceType": 0
}
{
"enabled": true,
"access": 4,
"displayName": "Global Address List",
"ownerUsername": "",
"itemID": "gal",
"isPrimary": false,
"shareType": "AddressBook",
"folderId": -1,
"isSharedItem": true,
"isDomainResource": false,
"sharedResourceType": 0
}
Selected source:
{
"enabled": true,
"access": 8,
"displayName": "Contacts",
"ownerUsername": "zach",
"itemID": "8477aee4e86b422bb7d8e2aeeb774ccb",
"isPrimary": true,
"shareType": "AddressBook",
"folderId": 460002,
"isSharedItem": false,
"isDomainResource": false,
"sharedResourceType": 0
}
Contacts:
{
"totalCount": 1,
"results": [
{
"hideFromLdap": false,
"categories": [],
"phoneNumberList": [],
"emailAddressList": [
"cool@blah.com"
],
"displayAs": "cool",
"picture": [],
"username": "zach",
"image": "api/v1/contacts/image?data=default",
"children": [],
"groupedUids": [],
"groupedContacts": [],
"csvFields": [],
"sourceOwner": "zach@example.com",
"sourceId": "8477aee4e86b422bb7d8e2aeeb774ccb",
"sourceName": "Contacts",
"flagInfo": {
"type": "None",
"iconColor": "Red"
},
"lastModifiedUtc": "2026-07-29T14:23:16.872517Z",
"id": "cbb4df0711d2413cb32e9aded4a91dab",
"nameAndEmail": "\"cool\" <cool@blah.com>"
}
],
"success": true,
"resultCode": 200
}
Zach@ZSYLVESTER-MBP ~ % Let me know if that helps.
Kind Regards,