Skip to main content

Upload Files via Multipart/Form-Data in Salesforce Apex: HTTP Callout Guide

In plain words: A multipart/form-data HTTP callout in Salesforce Apex is a technique used to send binary files (like PDFs, images, or documents) along with form parameters to an external REST API in a single HTTP request. Because Apex does not have a native multipart builder, developers assemble a raw binary body using unique delimiter boundaries, Base64/hex encoding, and proper Content-Type headers.

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 the Content-Type header 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.
360 Multipart Callout Architecture Card:
  • 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().

Step 1: Production-Ready Apex Service (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.

Step 2: Processing Salesforce Files for Callout (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

Encoding Trap: Setting Base64 Encoded Strings in setBody()
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().
Core Rule: Use hex string concatenation (EncodingUtil.convertToHex) to join text headers with binary file data before converting back to a Blob, and use Named Credentials to secure authentication headers.
  • 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 large errors.
  • Asynchronous Callouts for Large Files: For larger files (up to 6 MB), execute the callout inside a Queueable Apex class implementing Database.AllowsCallouts to take advantage of the 12 MB asynchronous heap limit.
  • Manage Authentication with Named Credentials: Replace hardcoded URLs and auth headers with callout:MyNamedCredential in 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.