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.