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.

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: https://social.technet.microsoft.com/Forums/en-US/26f6a32e-e0e0-48f8-b777-06c331883555/invokewebrequest-encoding?forum=winserverpowershell
which nicely pointed me here:
https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/13685217-invoke-restmethod-and-invoke-webrequest-encoding-b
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:
https://gist.github.com/weipah/19bfdb14aab253e3f109
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:
###INITIALIZATIONS###
$global:CWcompany = "xxxcompanyname"
$global:CWprivate = "xxxprivatekey"
$global:CWpublic = "xxxpublickey"
$global:CWserver = "https://na.myconnectwise.net/v4_6_release/apis/3.0/system/documents"
##don't use the api- url here for the server##
###CW AUTH STRING###
[string]$Authstring = $CWcompany + '+' + $CWpublic + ':' + $CWprivate
$encodedAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($Authstring)));
###CW HEADERS###
$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 = (
"--$boundary",
"Content-Disposition: form-data; name=`"recordType`"$LF",
"Ticket",
"--$boundary",
"Content-Disposition: form-data; name=`"recordId`"$LF",
"6956",
"--$boundary",
"Content-Disposition: form-data; name=`"Title`"$LF",
"testingFINAL",
"--$boundary",
"Content-Disposition: form-data; name=`"file`"; filename=`"image001.jpg`"",
"Content-Type: application/octet-stream$LF",
$fileEnc,
"--$boundary--$LF"
) -join $LF
Invoke-RestMethod -Uri $CWserver -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines -Headers $headers
superhelpful! A few of us in the MSPGeek slack channel have been talking about figuring this out. I just tested and confirmed it worked for me. You should join us! Probably could share some helpful info, and not have you re-create the wheel on a few things.
March 28, 2019 at 4:39 pm