Connecting Salesforce to external document management systems (such as AWS S3, Google Drive, Box, or external OCR engines) frequently requires uploading files via standard HTTP POST requests formatted as multipart/form-data. While standard JSON REST APIs work well for raw data, multipart encoding allows you to stream binary payloads without corrupting file headers.
1. Anatomy of a Multipart/Form-Data Request
A valid multipart request consists of several distinct structural elements:
- Boundary String: A unique delimiter (e.g.,
----------------1234567890) specified in theContent-Typeheader that separates each parameter and file chunk. - Part Headers: Contains metadata such as
Content-Disposition: form-data; name="file"; filename="invoice.pdf"followed by the file's MIME type (e.g.,application/pdf). - Binary Body: The raw binary content of the file.
- Closing Footer: The boundary string appended with trailing hyphens (
--boundary--) signaling the end of the HTTP payload.
- Request Header:
Content-Type: multipart/form-data; boundary={boundary} - Payload Method:
req.setBodyAsBlob(bodyBlob) - Authentication: Named Credentials to manage tokens and endpoints securely.
- Governor Limits: Maximum 6 MB synchronous heap size (12 MB asynchronous) for binary payloads.
2. Implementing Multipart File Uploads in Apex
The standard way to build a pure binary payload in Apex without corrupting file bytes is to construct the multipart header and footer as hex strings, combine them with the hex-encoded file blob, and decode the final concatenated hex string back into a binary blob using EncodingUtil.convertFromHex().
MultipartUploadService.cls)
public with sharing class MultipartUploadService {
/**
* @description Sends a file to an external endpoint using multipart/form-data encoding
* @param fileName Name of the file with extension (e.g., 'invoice.pdf')
* @param fileBody Raw binary blob of the file
* @param contentType MIME type of the file (e.g., 'application/pdf', 'image/png')
* @return HttpResponse from the external server
*/
public static HttpResponse uploadFileToExternalService(String fileName, Blob fileBody, String contentType) {
if (fileBody == null || String.isBlank(fileName)) {
throw new IllegalArgumentException('File name and file content cannot be null.');
}
// 1. Generate unique boundary
String boundary = '----------------------------' + String.valueOf(Crypto.getRandomLong()).replace('-', '');
// 2. Build header and footer as standard strings
String header = '--' + boundary + '\r\n' +
'Content-Disposition: form-data; name="file"; filename="' + fileName + '"\r\n' +
'Content-Type: ' + (String.isNotBlank(contentType) ? contentType : 'application/octet-stream') + '\r\n\r\n';
String footer = '\r\n--' + boundary + '--\r\n';
// 3. Convert header, binary payload, and footer to Hex to concatenate safely
String headerHex = EncodingUtil.convertToHex(Blob.valueOf(header));
String bodyHex = EncodingUtil.convertToHex(fileBody);
String footerHex = EncodingUtil.convertToHex(Blob.valueOf(footer));
// 4. Assemble the complete payload Blob
Blob fullBodyBlob = EncodingUtil.convertFromHex(headerHex + bodyHex + footerHex);
// 5. Configure the HTTP Request using Named Credentials
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:External_Document_API/v1/files/upload');
req.setMethod('POST');
req.setHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
req.setHeader('Content-Length', String.valueOf(fullBodyBlob.size()));
req.setBodyAsBlob(fullBodyBlob);
req.setTimeout(60000); // 60 seconds
Http http = new Http();
return http.send(req);
}
}
3. Uploading Salesforce Files (ContentVersion) via Apex
To upload files stored in Salesforce Files, query the latest ContentVersion record and pass its binary VersionData directly to the service.
DocumentSyncController.cls)
public with sharing class DocumentSyncController {
@AuraEnabled
public static String syncContentVersion(Id contentVersionId) {
ContentVersion cv = [
SELECT Id, Title, FileExtension, VersionData, FileType
FROM ContentVersion
WHERE Id = :contentVersionId
WITH USER_MODE
LIMIT 1
];
String fullFileName = cv.Title + '.' + cv.FileExtension;
String mimeType = 'application/' + cv.FileExtension.toLowerCase();
HttpResponse res = MultipartUploadService.uploadFileToExternalService(
fullFileName,
cv.VersionData,
mimeType
);
if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
return 'File uploaded successfully. Server response: ' + res.getBody();
} else {
throw new AuraHandledException('Upload failed with status ' + res.getStatusCode() + ': ' + res.getBody());
}
}
}
4. Common Traps & Platform Limitations
Calling
req.setBody(header + EncodingUtil.base64Encode(fileData) + footer) sends a string of Base64 characters rather than actual binary bytes. Unless the receiving endpoint specifically expects a Base64-encoded string, the external server will store a corrupted, unreadable file. Always assemble binary payloads using hex conversions and set the body with req.setBodyAsBlob().
- Apex Heap Size Limits: Converting files to hex strings doubles the required heap space in memory. Keep synchronous file callouts under 3 MB to prevent
LimitException: Apex heap size too largeerrors. - Asynchronous Callouts for Large Files: For larger files (up to 6 MB), execute the callout inside a Queueable Apex class implementing
Database.AllowsCalloutsto take advantage of the 12 MB asynchronous heap limit. - Manage Authentication with Named Credentials: Replace hardcoded URLs and auth headers with
callout:MyNamedCredentialin Setup to prevent leaking API keys and manage environment transitions cleanly.
Summary
Executing multipart/form-data uploads directly from Salesforce Apex enables integration with external cloud storage and document processors. By constructing clean multipart boundaries, converting file data safely with hex encoding, and securing endpoints via Named Credentials, developers can stream binary files reliably while staying well within Salesforce platform limits.