~/2019/01/09/powershell-connectwise-documents-api-uploading-a-document-or-attachment-to-a-ticket.md
PowerShell: ConnectWise Documents API, Uploading a Document or Attachment to a Ticket
--- author: Tom Lasswell date: updated: read: 2 min in: [ps, scripts] tags: [connectwise, powershell] ---

$ grep -n '^#' post.md
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.
Requirements
- PowerShell 5.1 or later, with
Invoke-RestMethod. - A ConnectWise API member with its public key, private key and your company identifier. Creating one is covered in Query Contacts by Email Address. The member's security role must be allowed to add documents to the record you upload to.
- Outbound HTTPS to the ConnectWise documents endpoint on
na.myconnectwise.net. Use the regular host here, not theapi-one. - The file to upload, readable from the machine running the script, and the ID of the record (for example a ticket) it should be attached to.
Parameters
The script has no parameters. Edit these values in it before running.
| Name | Type | Required | Description |
|---|---|---|---|
$global:CWcompany | string | Yes | Your CW company identifier, <company-id>. |
$global:CWprivate | string | Yes | The API member's private key, <private-key>. |
$global:CWpublic | string | Yes | The API member's public key, <public-key>. |
$global:CWserver | string | Yes | The documents endpoint URL, https://na.myconnectwise.net/v4_6_release/apis/3.0/system/documents. Despite the name it is the full endpoint, and it must not use the api- host. |
$FilePath | string | Yes | Full path of the file to upload, for example C:\path\to\file.jpg. |
recordType | string | Yes | Form field in $bodyLines: the kind of record to attach to. Ticket in this script. |
recordId | string | Yes | Form field in $bodyLines: the ID of that record, <record-id>. |
Title | string | Yes | Form field in $bodyLines: the title of the document in CW, <document-title>. |
Walkthrough
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)
Multipart boundaries
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.
Usage
Edit the values listed above, save the script as Add-CWDocument.ps1 and run it from a PowerShell prompt.
.\Add-CWDocument.ps1
Invoke-RestMethod prints whatever the CW documents API returns for the upload. To attach to a different record, change recordType and recordId in $bodyLines.
Script
Enjoy, here’s the full code layout:
<#
.SYNOPSIS
Uploads a file to a ConnectWise record (for example a ticket) through the Documents API.
.DESCRIPTION
Reads the file at $FilePath, re-encodes its bytes as ISO 8859-1 (code page 28591) so
Invoke-RestMethod does not garble them, builds a multipart/form-data body by hand
(recordType, recordId, Title and the file) and posts it to the ConnectWise
system/documents endpoint using an API member's keys.
.EXAMPLE
.\Add-CWDocument.ps1
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.1 (2026-09-20)
History : 1.0 2019-01-09 Initial version.
1.1 2026-09-20 The uploaded file name now comes from $FilePath.
Requires: PowerShell 5.1 or later; ConnectWise API member with permission to add documents
.LINK
https://gist.github.com/weipah/19bfdb14aab253e3f109
#>
# Initializations
$global:CWcompany = "<company-id>"
$global:CWprivate = "<private-key>"
$global:CWpublic = "<public-key>"
# Don't use the api- URL here for the server.
$global:CWserver = "https://na.myconnectwise.net/v4_6_release/apis/3.0/system/documents"
# 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")
# Read the file and build the multipart body
$FilePath = 'C:\path\to\file.jpg'
$fileName = [System.IO.Path]::GetFileName($FilePath)
$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",
"<record-id>",
"--$boundary",
"Content-Disposition: form-data; name=`"Title`"$LF",
"<document-title>",
"--$boundary",
"Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"",
"Content-Type: application/octet-stream$LF",
$fileEnc,
"--$boundary--$LF"
) -join $LF
# Upload
Invoke-RestMethod -Uri $CWserver -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -Body $bodyLines -Headers $headers
Notes
- Update 2026-09-20 (v1.1): the file name in the multipart body was hard-coded as
image001.jpg, so every upload was named that way whatever$FilePathpointed at. It is now taken from$FilePath.