Phew, this one took a minute to figure out. ConnectWise has a form based documents API (technically not really API, but it’s the way you get yourself a document into a CW ticket). First is the really amazing documentation that CW provides around the documents API

Second is then working with PowerShell to handle streamed encoding correctly, build a multipart form data payload, and then getting it to actually send the correct thing. Ultimately there were some good learning steps here. Mainly on how to construct a proper content type of “multipart/form-data”. I’m writing this in hopes that many of you that are out there that are facing a similar challenge on getting documents to upload into CW via PowerShell aren’t faced with the same 2 day challenge I just had.

Encoding Issues

Mainly the Encoding Issues were around reading a file into PowerShell and then using Invoke-RestMethod to send it off. Typically you’d work in UTF-8, while that’s great in PS when working, sending that encoding via the Invoke-RestMethod seems to break things a little and none of the characters are correct, thus resulting in a data stream sent to your destination being garbled.

Left – Proper Data | Right – Garbled Data

I happened to stumble, and by stumble, I’ve been searching the Google masters for quite a while trying to understand why the encoding wasn’t working correctly, upon this article:

which nicely pointed me here:

Taking from this, I modified the following from:

$fileEnc = [System.Text.Encoding]::GetEncoding('UTF-8').GetString($fileBytes);

To using the ISO 8859-1 encoding type of 28591. Converting this line to:

$fileEnc = [System.Text.Encoding]::GetEncoding(28591).GetString($fileBytes);

The rest of the time was learning to deal with boundaries in a multipart/form-data payload. Essentially finding this article:

This taught me a bit about the boundaries that need to be set and more-so having to use “`r`n” in different places, you’ll see this referenced the same way as in the link in my script below using the “$LF” variable.

Enjoy, here’s the full code layout:

$global:CWcompany    = "xxxcompanyname"
$global:CWprivate    = "xxxprivatekey"
$global:CWpublic     = "xxxpublickey"
$global:CWserver     = ""
##don't use the api- url here for the server##
[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")

$FilePath = 'c:\users\Tom\Desktop\image001.jpg'
$fileBytes = [System.IO.File]::ReadAllBytes($FilePath);
$fileEnc = [System.Text.Encoding]::GetEncoding(28591).GetString($fileBytes);
$boundary = [System.Guid]::NewGuid().ToString(); 
$LF = "`r`n";

$bodyLines = ( 
    "Content-Disposition: form-data; name=`"recordType`"$LF",
    "Content-Disposition: form-data; name=`"recordId`"$LF",
    "Content-Disposition: form-data; name=`"Title`"$LF",
    "Content-Disposition: form-data; name=`"file`"; filename=`"image001.jpg`"",
    "Content-Type: application/octet-stream$LF",
) -join $LF

Invoke-RestMethod -Uri $CWserver -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines -Headers $headers