PowerShell FTP: FtpWebRequest, the WinSCP .NET Assembly, and curl.exe for FTP and FTPS
PowerShell does not have a Send-FtpFile cmdlet. The right tool depends on your PowerShell version, the protocol the server requires, and how much reliability you need. Here are the three options and how to pick.
PowerShell does not have a native Send-FtpFile cmdlet. The right tool depends on your PowerShell version, the protocol the server requires, and how much reliability you need.
The short version: FtpWebRequest for maintaining an existing Windows PowerShell 5.1 script, the WinSCP .NET assembly for building a reliable FTP, FTPS, or SFTP workflow, curl.exe for a quick single-file transfer, and a Files.com Remote Server Mount for taking FTP credentials and scheduling off the Windows machine altogether.
One distinction before starting: FTP, FTPS, and SFTP are different protocols. FTP sends credentials and data without encryption, FTPS adds TLS to FTP, and SFTP runs over SSH and needs a different client. Use FTPS or SFTP instead of plain FTP whenever you can, and for SFTP from PowerShell, the PowerShell SFTP post covers Posh-SSH and the rest.
FtpWebRequest in Windows PowerShell 5.1
Windows PowerShell 5.1 runs on the .NET Framework, which includes the System.Net.FtpWebRequest class. No module to install, which makes it useful for existing scripts and environments where adding software is difficult.
Upload a file:
$uri = "ftp://ftp.harborline.example/inbound/orders.csv"
$request = [System.Net.FtpWebRequest]::Create($uri)
$request.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$request.Credentials = New-Object System.Net.NetworkCredential("dana", $env:FTP_PASSWORD)
$request.UseBinary = $true
$request.UsePassive = $true
$fileStream = [System.IO.File]::OpenRead("C:\exports\orders.csv")
try {
$requestStream = $request.GetRequestStream()
try { $fileStream.CopyTo($requestStream) } finally { $requestStream.Dispose() }
$response = $request.GetResponse()
try { Write-Host $response.StatusDescription } finally { $response.Dispose() }
}
finally {
$fileStream.Dispose()
}
Streams instead of ReadAllBytes() keep the whole file out of memory, which matters once transfers pass a few megabytes.
Download by setting the method to DownloadFile and copying $request.GetResponse().GetResponseStream() to a local file. The other supported methods are ListDirectory, ListDirectoryDetails, DeleteFile, Rename, MakeDirectory, and RemoveDirectory.
For explicit FTPS, add $request.EnableSsl = $true. FtpWebRequest connects on port 21 and upgrades with AUTH TLS; it does not do implicit FTPS on port 990. It validates the server's TLS certificate against the Windows store, and that validation stays on. The often-suggested workaround of replacing ServerCertificateValidationCallback with a function that always returns $true disables certificate validation for the entire PowerShell process, which is not a production fix. Install the right CA certificate, correct the server's certificate, or use a client that pins certificates.
In PowerShell 7, which runs on modern .NET, FtpWebRequest still exists and is marked obsolete. It works today. Keep it for existing Windows PowerShell 5.1 scripts, and choose WinSCP or curl.exe for new automation.
WinSCP's .NET assembly
For production transfers, the WinSCP .NET assembly is the strongest option. It supports FTP, explicit and implicit FTPS, SFTP, and SCP, certificate and host-key pinning, directory synchronization, and detailed transfer results. WinSCP installs WinSCPnet.dll alongside its command-line application.
Upload with explicit FTPS:
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
Protocol = [WinSCP.Protocol]::Ftp
FtpSecure = [WinSCP.FtpSecure]::Explicit
HostName = "ftp.harborline.example"
UserName = "dana"
Password = $env:FTP_PASSWORD
# Paste the exact TLS certificate fingerprint WinSCP reports.
TlsHostCertificateFingerprint = "<certificate fingerprint>"
}
$session = New-Object WinSCP.Session
try {
$session.Open($sessionOptions)
$result = $session.PutFiles("C:\exports\orders.csv", "/inbound/")
$result.Check()
foreach ($transfer in $result.Transfers) { Write-Host "Uploaded $($transfer.FileName)" }
}
finally {
$session.Dispose()
}
The certificate fingerprint is the right way to handle a partner's self-signed certificate: instead of turning validation off, the script accepts only the expected certificate. For implicit FTPS, set FtpSecure to [WinSCP.FtpSecure]::Implicit. Download with $session.GetFiles("/outbound/results.csv", "C:\imports\").Check().
Both PutFiles() and GetFiles() return result objects, and Check() throws if any transfer failed, so a broken transfer stops the script instead of passing silently. WinSCP's synchronization methods mirror whole directory trees, which is far easier to maintain than listing, comparing, uploading, and deleting by hand with FtpWebRequest. The WinSCP command line and scripting post covers the same tool's script mode.
curl.exe
Windows 10 version 1803 and later include curl.exe. It is the quickest option when a larger PowerShell script has to perform one transfer and move on.
Upload with explicit FTPS:
& curl.exe --silent --show-error --ssl-reqd `
--user "dana:$env:FTP_PASSWORD" `
--upload-file "C:\exports\orders.csv" `
"ftp://ftp.harborline.example/inbound/orders.csv"
if ($LASTEXITCODE -ne 0) { throw "curl upload failed with exit code $LASTEXITCODE" }
ftp:// with --ssl-reqd requests explicit FTPS and refuses to continue without TLS. Download with --output instead of --upload-file. For implicit FTPS, use an ftps:// URL. Always check $LASTEXITCODE; PowerShell does not turn a nonzero exit code from a native executable into a terminating error on its own. curl.exe --version lists the supported protocols, and the build bundled with Windows supports FTP and FTPS but not SFTP. The curl FTP, FTPS, and SFTP post goes deeper on the flags.
Keep credentials out of the script
None of the examples hard-code the password; they read $env:FTP_PASSWORD. That beats a password in a .ps1 file, and a scheduled task still needs a secure way to obtain the value: Windows Credential Manager, a DPAPI-encrypted credential file, a secrets-management platform, a PowerShell SecretManagement vault, or a managed file transfer platform that stores the remote credentials.
To store a credential with DPAPI, run this once while signed in as the account that will run the scheduled task:
$credential = Get-Credential
$credential | Export-Clixml "C:\Secure\harborline-ftp.xml"
The scheduled script imports it with Import-Clixml and calls GetNetworkCredential() for the username and password, or hands it straight to FtpWebRequest as $request.Credentials = $credential.GetNetworkCredential(). The exported password is protected with Windows DPAPI, so normally only the same Windows account on the same computer decrypts it; protect the file with NTFS permissions as well. Windows Credential Manager is another good option, usually reached from PowerShell through a module such as CredentialManager. A plaintext password in a shared .ps1 is exactly the problem an audit finds.
Make scheduled transfers survive production
One successful transfer is easy. Keeping the job reliable for months is the harder part.
Run the task under a dedicated service account with access only to the local directories, the credential store, the task, and the network resources it needs, not under a personal administrator account. Wrap the script in try and catch and exit nonzero on failure so Task Scheduler can tell success from failure: for WinSCP call Check(), for curl.exe inspect $LASTEXITCODE, and for FtpWebRequest let GetResponse() throw. Log the start and completion time, the paths, the size, the result, the error, and the retry count, and never the password or a credential-bearing command line. Retry only transient failures, network interruptions and temporary server errors, with a limited number of attempts and increasing delays; an infinite loop hides a broken integration for days. And monitor the business event, not only the script. A successful task proves the script ran, not that the partner supplied the file. If an inbound file is due every morning, watch for that file explicitly, because no retry loop recovers a file the partner never sent.
The option that removes FTP from PowerShell
Sometimes the most maintainable FTP script is no FTP script. A Files.com Remote Server Mount connects a partner's FTP or FTPS server to a folder on your Files.com site. Files.com stores the partner credentials, handles certificate verification, and passes file operations through to the remote server in real time. PowerShell then talks HTTPS instead of opening FTP connections from the Windows machine.
The Files.com CLI uploads to the mounted folder with resumable, parallel transfers:
files-cli upload "C:\exports\orders.csv" "/partners/harborline/inbound/"
PowerShell 7 scripts that want typed objects call the Files.com .NET SDK directly. In some workflows the Windows job disappears: a scheduled Sync moves the files, an Automation routes or renames them after arrival, the partner's FTP credentials stay in Files.com, the Windows server keeps one API key, and every transfer appears in the audit log. That is particularly useful when several scripts connect to different partners, each with its own credentials, certificates, schedules, and retry rules.
Which PowerShell FTP option to choose
Use FtpWebRequest when you are maintaining an existing Windows PowerShell 5.1 script and cannot install another client. Use the WinSCP .NET assembly when reliability, protocol support, certificate pinning, or directory synchronization matters. Use curl.exe for a simple upload or download inside a larger script, provided you check its exit code. Use a managed file transfer platform when you want to move credentials, scheduling, retries, and audit history off the Windows machine altogether.
Frequently asked questions
How do I upload a file to an FTP server with PowerShell?
In Windows PowerShell 5.1, create a System.Net.FtpWebRequest, set its method to UploadFile, assign credentials, and write the file to GetRequestStream(). For new scripts, WinSCP's PutFiles() is easier to run reliably. For a one-off transfer, use curl.exe --upload-file.
Does PowerShell have a built-in FTP command?
No. PowerShell has no native FTP cmdlet. Windows PowerShell 5.1 uses the .NET Framework's FtpWebRequest class, and modern Windows includes curl.exe, which PowerShell calls directly.
How do I use FTPS from PowerShell?
For explicit FTPS with FtpWebRequest, set $request.EnableSsl = $true; it connects on port 21 and upgrades with TLS. For implicit FTPS on port 990, use WinSCP with FtpSecure set to Implicit, or curl.exe with an ftps:// URL.
How do I keep the FTP password out of my PowerShell script?
Retrieve it at runtime from Windows Credential Manager, a DPAPI-encrypted file created with Export-Clixml, a PowerShell secrets vault, or another secrets-management system. Never store it in the script or a plaintext configuration file.
Can FtpWebRequest connect to an SFTP server?
No. SFTP is an SSH-based protocol that FtpWebRequest does not support. For SFTP from PowerShell, use the WinSCP .NET assembly, Posh-SSH, or the OpenSSH sftp.exe included with Windows.
Keep reading
- PowerShell SFTP: Three Practical Ways to Automate File Transfers on WindowsPowerShell SFTP with working code: the Posh-SSH module, the OpenSSH sftp client built into Windows, and a third way where Files.com makes the SFTP connection for you. SSH keys, host verification, scheduled-task credentials, and a production-ready script.
- WinSCP Command Line and Scripting: Automate File Transfers With WinSCP.com and .NETWinSCP scripting and automation: the WinSCP.com CLI, scripted session syntax, .NET assembly for deeper integration, logging, SSH key auth, and the failure-mode design that makes scheduled jobs actually reliable.
- curl FTP, FTPS, and SFTP: Upload, Download, and Script Transfers From the Command Linecurl speaks FTP, FTPS, and, depending on the build, SFTP. The commands for uploading and downloading over each, the flags that matter, why the macOS and Windows curls lack SFTP, how to keep credentials out of the command, and how to run any of it from a scheduled job.
- FTPS Explained: Implicit vs Explicit FTPS, Ports 990 and 21, and What the Certificate DoesFTPS is FTP secured with TLS: not SFTP, not a new protocol, and heir to every quirk of the FTP it encrypts. How explicit and implicit FTPS differ, which ports each uses, what the certificate verifies, why firewalls struggle with it, and when it is still the right choice.