Error getting contacts using API
Problem reported by Somo IT - 7/25/2026 at 12:19 AM
Resolved
Im trying to get a contact details using...
Documentation/api#/reference/SmarterMail.Web.Api.ContactsController/GetContact/get

It works ok for gal contacts. For example:

Path: 
/api/v1/contacts/contact/0f549615105040079dbef2007a131050/gal

Response: 
{'hideFromLdap': False, 'categories': [], 'phoneNumberList': [], 'emailAddressList': ['admin@mydomain.com'], 'displayAs': 'admin', 'picture': [], 'image': 'api/v1/contacts/image?data=default', 'children': [], 'groupedUids': [], 'groupedContacts': [], 'csvFields': [], 'sourceId': 'gal', 'flagInfo': {'type': 'None', 'iconColor': 'Red'}, 'lastModifiedUtc': '2026-07-25T06:56:33.0805671Z', 'id': 'a9d451633b4c44eb96da712ca21ed749', 'nameAndEmail': 'admin', 'sourcePermission': 4}

But for contacts in any other folder, for example for an user with id "0fa8faf3b85241a49b835090055e3587" in folder "Contacts" with source_id "33591f1bcc1341709118bd060e85cb43" it always returns

{"success":false,"resultCode":400,"message":"Invalid Access"}

I have tried multiple different combinations of paths:

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/33591f1bcc1341709118bd060e85cb43/contacto1@blabla.com

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/33591f1bcc1341709118bd060e85cb43

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/33591f1bcc1341709118bd060e85cb43/test@mydomain.com

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/Contacts

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/Contacts/contacto1@blabla.com

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/Contacts/test@mydomain.com

Similar behaviour happens with the api/v1/contacts/fetch-many" 
Andrew Barker Replied
Employee Post
Is the authentication token you are including for the API call for the same user whose contacts you are trying to access? Or does the authentication token point to a user with share permissions for accessing the contact folder? The invalid access response typically indicates that the user indicated by the authentication token doesn't have sufficient permissions to perform the requested operation.

Andrew Barker
Lead Software Developer
SmarterTools Inc.
www.smartertools.com 

Somo IT Replied
Hello Andrew. Thanks for the reply.

Yes the auth token is obtained logging in with the user that is owner of the mailbox including calendar. In fact i can create and delete contacts in folder "Contacts". But i cannot get the details.

The user has access "8"  (?):
{'enabled': True, 'access': 8, 'displayName': 'Contacts', 'ownerUsername': 'test', 'itemID': '33591f1bcc1341709118bd060e85cb43', 'isPrimary': True, 'shareType': 'AddressBook', 'folderId': 50002, '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}
"access": number,// The access level the user has to this contact source, determining read/write permissions.  [Required]


Andrew Barker Replied
Employee Post
An `access` level of 8 indicates manage permissions, but that only really matters when `isSharedItem` is true. Since `isSharedItem` is false, indicating an owned folder, you should have full read/write permissions.

One thing to note for `/api/v1/contacts/contact/{contactId}/{folderId}/{email}` is that the email parameter should be the owner's username, not their full email. So, using one of the examples from your first post, it should look like:

/api/v1/contacts/contact/0fa8faf3b85241a49b835090055e3587/33591f1bcc1341709118bd060e85cb43/test

That may be all you need to change to fix your issue. I'll add a task to our system to update the variable naming and documentation for that call to clarify that the API is expecting a username and not an email.

Andrew Barker
Lead Software Developer
SmarterTools Inc.
www.smartertools.com 

Zach Sylvester Replied
Employee Post Marked As Resolution
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, 

Zach Sylvester

Software Developer
SmarterTools Inc.
Somo IT Replied
Thanks both for your help! I got it working!

Reply to Thread

Enter the verification text