PowerShell

PowerShell: ConnectWise REST API Query Contacts by Email Address

Originally published on TechColumnist.

PowerShell: ConnectWise REST API Query Contacts by Email Address

I’ve found myself at a new job, recreating many of the processes that I spent the last few years putting together, tweaking, modifying, building a new managed services provider with an exciting new company. One of those challenges lead me to Email parsing and ConnectWise (CW). Previously I had the opportunity to use Autotask and email2ticket, however with the modifications to the API that CW did a few years ago, email2ticket is no longer supported for CW. With my found love of Azure Functions and looking at existing mail based parsing tools (https://mailparser.io/, https://www.thinkautomation.com/) which have amazing feature sets, they didn’t do quite what I was looking for and to replace the functionality that I once had with email2ticket.

That is leading me to this PowerShell series for how to utilize the CW REST API and the things that took some understanding and digging in a little to determine how to do simple queries. The CW developer portal has some great resources, and I stumbled upon the forums that ultimately made it possible to finally build a wildcard query via PowerShell to identify whether a contact exists in CW.

Requirements

  • PowerShell 5.1 or later, with Invoke-RestMethod.
  • A CW API Access Account (an API member on the Members tab) with its public key, private key and your company identifier. First things first, you have to authenticate to the CW REST API, and you need access to the Admin Setup tables to create the account. The member's security role must be able to read contacts.
  • An Azure Functions PowerShell function with an HTTP trigger binding. The script reads the email query string parameter from $req_query_email and writes its JSON response to the output binding file path in $res.
  • Outbound HTTPS from the function to your CW API host (api-na.myconnectwise.net for the North America cloud).

ConnectWise Manage Members screen with the API Members tab highlighted by a red arrowCW Members Tab – API Members

Parameters

NameTypeRequiredDescription
emailstringYesFull or partial email address to search for, for example @xyz.corp for every contact in a domain. Passed in the query string and read from $req_query_email.
$global:CWcompanystringYesValue to edit: your CW company identifier, <company-id>.
$global:CWprivatestringYesValue to edit: the API member's private key, <private-key>.
$global:CWpublicstringYesValue to edit: the API member's public key, <public-key>.
$global:CWserverstringYesValue to edit: the base URL of your CW API host, for example https://api-na.myconnectwise.net.

Walkthrough

Once you have an integration setup you can proceed with creating a PowerShell script to handle the automation. I build in Azure Functions mostly, so there will be some pieces in here that relate to that, I’ll breakdown each section (and eventually move some of these pieces to linked articles).

Authentication

Authentication to the CW rest API is fairly simple. It requires your public and private keys and your company identifier.

First set your variables for your credentials.

powershell
$global:CWcompany    = "<company-id>"
$global:CWprivate    = "<private-key>"
$global:CWpublic     = "<public-key>"
$global:CWserver     = "https://api-na.myconnectwise.net"

Second configure the authentication string and setup the standard headers for your GET request.

powershell
[string]$Accept      = "application/vnd.connectwise.com+json; version=3.0"
[string]$ContentType = 'application/json'
[string]$Authstring  = $CWcompany + '+' + $CWpublic + ':' + $CWprivate
$encodedAuth         = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($Authstring)))

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Basic $encodedAuth")
$headers.Add("Content-Type", 'application/json')
$headers.Add("Accept", $Accept)

Request URI

Then for the URI, this consists of a few parts, your base URL, your query parameters, and your target for your request.

Query String – this is the conditions you’re going to pass to do the lookup. In this case I’m looking for the email address of a contact. This requires the use of CW’s childconditions parameters. Initially the communicationItems is used to determine the value of the email address, then forcing it to only look for the type of email address to speed the query result. The “%” is used as the wildcard for the “like” operator. I spent some time trying to determine what would work best, contains and in both resulted in invalid syntax so using the like operator was the eventual conclusion.

powershell
[string]$query       = '?childconditions=communicationItems/value like "%' + $email + '%" AND communicationItems/communicationType="Email"'

Putting it all together, you have a target of /company/contacts and use the query string. $email is the full or partial email address you’re searching for. Benefits of using a partial, such as @xyz.corp, would yield all contacts for that domain and you can use some logic there to determine what company they belong to (more to come on that subject).

powershell
[string]$TargetUri   = '/company/contacts'
[string]$query       = '?childconditions=communicationItems/value like "%' + $email + '%" AND communicationItems/communicationType="Email"'
[string]$BaseUri     = "$CWserver" + "/v4_6_release/apis/3.0" + $TargetUri + $query

Sending the request

Finally, send the Invoke-RestMethod command to get the results. This returns a JSON table that Invoke-RestMethod converts to a PS Object.

powershell
$JSONResponse = Invoke-RestMethod -Uri $BaseUri -Headers $headers -ContentType $ContentType -Method Get

Usage

Once the function is deployed, request it with the full or partial email address in the query string.

text
https://<function-app>.azurewebsites.net/api/<function-name>?email=<email>

The same call from PowerShell:

powershell
Invoke-RestMethod -Uri "https://<function-app>.azurewebsites.net/api/<function-name>?email=<email>" -Method Get

If the function's authorization level is not anonymous, add &code=<function-key> to the URL. A matching contact comes back in this shape:

text
{
    "id":  1234,
    "firstName":  "<first-name>",
    "lastName":  "<last-name>",
    "emails":  "<email>;",
    "company":  "<company-name>",
    "companyid":  56,
    "companyidentifier":  "<company-identifier>"
}

If nothing matches, the script returns $false and writes nothing to $res.

Script

Here’s the full code that I’m using to query for a specific contact’s email address.

powershell
<#
.SYNOPSIS
    Queries ConnectWise for contacts by full or partial email address.
.DESCRIPTION
    Runs as an Azure Functions PowerShell HTTP trigger. Reads the email address from the
    query string ($req_query_email), authenticates to the ConnectWise REST API with an API
    member's keys, searches /company/contacts with a wildcard "like" condition on the
    contact's email communication items, and writes the matching contacts (id, name,
    emails and company details) as JSON to the output binding file path in $res.
.PARAMETER email
    Full or partial email address to search for. Passed in the query string and read from $req_query_email.
.EXAMPLE
    Invoke-RestMethod -Uri "https://<function-app>.azurewebsites.net/api/<function-name>?email=<email>" -Method Get
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.1 (2026-09-20)
    History : 1.0 2018-12-27 Initial version.
              1.1 2026-09-20 The Accept header value no longer includes an "Accept: " prefix.
    Requires: PowerShell 5.1 or later; Azure Functions HTTP trigger ($req_query_email, $res); ConnectWise API member
.LINK
    https://developer.connectwise.com/products/manage/rest?a=Company&e=Contacts&o=GET
#>

# GET method: each querystring parameter is its own variable
if ($req_query_email) {
    $email = $req_query_email
}

# Initializations
$global:CWcompany    = "<company-id>"
$global:CWprivate    = "<private-key>"
$global:CWpublic     = "<public-key>"
$global:CWserver     = "https://api-na.myconnectwise.net"

# CW auth string
[string]$Accept      = "application/vnd.connectwise.com+json; version=3.0"
[string]$Authstring  = $CWcompany + '+' + $CWpublic + ':' + $CWprivate
[string]$ContentType = 'application/json'
$encodedAuth         = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($Authstring)))

# CW headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Basic $encodedAuth")
$headers.Add("Content-Type", 'application/json')
$headers.Add("Accept", $Accept)

# CW query
[string]$TargetUri   = '/company/contacts'
[string]$query       = '?childconditions=communicationItems/value like "%' + $email + '%" AND communicationItems/communicationType="Email"'
[string]$BaseUri     = "$CWserver" + "/v4_6_release/apis/3.0" + $TargetUri + $query

# Get response
$JSONResponse = Invoke-RestMethod -Uri $BaseUri -Headers $headers -ContentType $ContentType -Method Get

# Parse contact info to usable short table
$contactInfo = @()
foreach ($contact in $JSONResponse) {
    $email = $null
    $emails = $null
    $obj = New-Object PSObject
    $obj | Add-Member -MemberType NoteProperty -Name "id" -Value $contact.id
    $obj | Add-Member -MemberType NoteProperty -Name "firstName" -Value $contact.firstName
    $obj | Add-Member -MemberType NoteProperty -Name "lastName" -Value $contact.lastName
    foreach ($commtype in $contact.communicationItems) {
        $email = $($commtype | Where-Object { $_.communicationType -eq "Email" }).value
        if ($email.length -gt 2) {
            $emails += $email + ";"
        }
    }
    $obj | Add-Member -MemberType NoteProperty -Name "emails" -Value $emails
    $obj | Add-Member -MemberType NoteProperty -Name "company" -Value $contact.company.name
    $obj | Add-Member -MemberType NoteProperty -Name "companyid" -Value $contact.company.id
    $obj | Add-Member -MemberType NoteProperty -Name "companyidentifier" -Value $contact.company.identifier
    $contactInfo += $obj
}

if ($contactInfo) {
    Out-File -Encoding Ascii -FilePath $res -InputObject $($contactInfo | ConvertTo-Json)
} else {
    return $false
}

Notes

  • Update 2026-09-20 (v1.1): $Accept used to be "Accept: application/vnd.connectwise.com+json; version=3.0", which put a second Accept: inside the header value. It is now just the media type.