Upload a file to Connectwise and Attach it to a Service Ticket with PowerShell
Table of Contents
I have recently been automating a lot within Connectwise PSA. One of the items I set out to do is to upload a file and attach it to a service ticket. This led me to the following article, but after doing some testing, I found that some file types were not properly rendering on the Connectwise side, making me believe there was something wrong with the encoding.
I could upload a .txt
file without issues, but I also tried with a .docx
and a .pdf
, and the file would be corrupted or blank.
The process to upload a file and then link it to a service ticket is first to upload the file to the endpoint /system/documents
and then, from there, link the uploaded document to an existing service ticket.
Multipart/Form-Data
The first thing to know about how Connectwise wants a document uploaded is that it uses what is known as multipart/form-data
, which is used for uploading files and sending data through an HTTP POST request. Multipart/form-data is a MIME type used to send data to a server in a web form. The important thing to note when working with multipart/form-data
is that the data is separated by boundaries (typically a string of characters) generated by the browser or other client software. The boundary strings are used to identify the beginning and end of each part.
In my example below, we can see how the body is structured. I have a randomly generated GUID (created by running [System.Guid]::NewGuid().ToString()
) that is separating my data. The payload for each dataset is contained on its own line. my recordType
is Ticket, recordID
is 6630642, and the last area after application/octet-stream
shows the contents of my file.
When you run Invoke-Restmethod
, you need to tell it what designates the boundary so the server knows where data begins and ends. This is done using the -ContentType
parameter in the POST
:
$POST = Invoke-RestMethod -Uri $BaseURL -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyBytes -Headers $headers
--61f97753-f20e-488c-b6b5-28cd626fe0eb
Content-Disposition: form-data; name="recordType"
Ticket
--61f97753-f20e-488c-b6b5-28cd626fe0eb
Content-Disposition: form-data; name="recordId"
6630642
--61f97753-f20e-488c-b6b5-28cd626fe0eb
Content-Disposition: form-data; name="Title"
--61f97753-f20e-488c-b6b5-28cd626fe0eb
Content-Disposition: form-data; name="file"; filename="BRAD WYATT TESTING.txt"
Content-Type: application/octet-stream
THis is line 1!
This is line 2
This is line 4.
--61f97753-f20e-488c-b6b5-28cd626fe0eb--
Encoding
File Encoding
The next thing to consider is file encoding. In the article I linked above, they encode the file using encoding 28591
. I changed it to ISO 8859-1
, which is essentially the same thing. There are some minor differences between the two; for example, encoding 28591
includes the euro symbol (€), while ISO 8859-1
does not.
In the example below, we first read all the bytes from a file into a byte array. From there, we encode the byte array into a specific character encoding. Finally, this character encoding will be added to our multi-form body (not shown).
$FilePath = $FileToUpload
$fileBytes = [System.IO.File]::ReadAllBytes($FilePath);
$fileEnc = [System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($fileBytes);
Body Encoding
The missing piece, which was why my files would be corrupted or blank on the Connectwise side, was encoding the Multipart/Form-Data
body. The original article was sending the variable $bodylines
without encoding it. Once I added the line below, which encoded the entire body using encoding iso-8859-1
it would properly render the attachment (.png, jpg, .pdf, .txt, .docx, etc) on the Connectwise side.
$bodyBytes = [System.Text.Encoding]::GetEncoding("iso-8859-1").GetBytes($bodyLines)
Code
The Connectwise API uses basic authentication. You will need to create an API member and Client ID to use. The auth string is a base64 encoded string that is “companyid+publickey:privatekey” of your API member. The “+” and “:” are to be kept in the string, the “companyid”, “publickey” and “privatekey” are all unique to your instance. I’m lazy, so I just encoded mine using the following website.
function Upload-CWDocument {
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[object]$FileToUpload,
[Parameter(Mandatory)]
[system.string]$CWFileName,
[Parameter(Mandatory)]
[system.string]$TicketNumber,
[Parameter(Mandatory)]
[system.string]$CWServer,
[Parameter()]
[system.string]$CWFileDescription,
[Parameter(Mandatory)]
[system.string]$clientID,
[Parameter(Mandatory)]
[system.string]$authString
)
begin {
#region Headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
# ClientID is the API key generated for the application at developer.connectwise.com
$headers.Add("clientID", $clientID)
# Authorization is a public and private API key converted into ASCII encoding per Connectwise API documentation
$headers.Add("Authorization", "Basic $authString")
# CompanyName defines the database you are working with
#$headers.Add("companyName", "<companyName")
# Defines application content type
$headers.Add("Content-Type", "application/json")
# Define API versioning
$headers.Add("Accept", "application/vnd.connectwise.com+json; version=2024.1")
#endregion Headers
$BaseURL = "https://$CWServer/v4_6_release/apis/3.0/system/documents"
#The file comes in as a base64 string, store the value in a variable
$FilePath = $FileToUpload
$fileBytes = [System.IO.File]::ReadAllBytes($FilePath);
$fileEnc = [System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($fileBytes);
$boundary = [System.Guid]::NewGuid().ToString();
$LF = "`r`n";
$bodyLines = (
"--$boundary",
"Content-Disposition: form-data; name=`"recordType`"$LF",
"Ticket",
"--$boundary",
"Content-Disposition: form-data; name=`"recordId`"$LF",
"$TicketNumber",
"--$boundary",
"Content-Disposition: form-data; name=`"Title`"$LF",
"$CWFileName",
"--$boundary",
"Content-Disposition: form-data; name=`"file`"; filename=`"$CWFileName`"",
"Content-Type: application/octet-stream$LF",
$fileEnc,
"--$boundary--$LF"
) -join $LF
# Convert the body into bytes
$bodyBytes = [System.Text.Encoding]::GetEncoding("iso-8859-1").GetBytes($bodyLines)
}
process {
$POST = Invoke-RestMethod -Uri $BaseURL -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyBytes -Headers $headers
}
end {
Write-Output $POST
}
}
$CWSplat = @{
FileToUpload = "/Users/bradley.wyatt/Desktop/test.pdf"
CWFileName = "test.pdf"
TicketNumber = "6652732"
CWServer = "cwm-prod.bwyatt.com"
clientID = "fds87f6e-sd7986f-454-5345-ff89dsf67"
authString = "HGFDSf78dfsoibhsdbfo8d7ft8JNHDF78"
CWFileDescription = "This is a test document"
}
Upload-CWDocument @CWSplat
Response
Once you upload the document successfully, the server will return the following information:
id : 860454
title : test.pdf
fileName : test.pdf
serverFileName : 78458-2fae-448-b492-854538c68ed.pdf
owner : bwyatt
linkFlag : False
imageFlag : False
publicFlag : False
htmlTemplateFlag : False
readOnlyFlag : False
size : 78880
urlFlag : False
createdOnDate : 5/9/2024 6:42:55 PM
documentType : @{id=14; name=pdf; _info=}
guid : 6345d06-eab1-4349f-3416-985b3443390
_info : @{lastUpdated=5/9/2024 6:42:55 PM; updatedBy=bwyatt}
My name is Bradley Wyatt; I am a 5x Microsoft Most Valuable Professional (MVP) in Microsoft Azure and Microsoft 365. I have given talks at many different conferences, user groups, and companies throughout the United States, ranging from PowerShell to DevOps Security best practices, and I am the 2022 North American Outstanding Contribution to the Microsoft Community winner.
One thought on “Upload a file to Connectwise and Attach it to a Service Ticket with PowerShell”
Very cool. I haven’t found the need to upload a file yet, but I will definitely reference this if and when I do!