Retrieve SFDC Attachments using REST

Salesforce changed the way files are managed with Lightning Spring 17′ release, and this introduced a more elaborate procedure to reach the files from external systems. The API call is not as straight-forward as retrieving the ‘body’ to get file content (which was the case earlier).

We will discuss a bit about how to retrieve the files using SFDC APIs. I will be using Javascript to demonstrate, but the API calls should remain similar elsewhere.

Before jumping in the code for API call, explore how the content is stored. Check out how Contact attachment data is stored.

salesforce notes and attachments

Create a SFDC Connected App, and prepare your REST client. Send a post request to authenticate, which will get you the authentication token (access_token) and the instance URL (instance_url) – make note of them. (instructions are in the previously linked post)

Once authenticated, point the URL to do a get request.

https://<instance_url>/services/data/v43.0/query?q=select id, LinkedEntityId,ContentDocumentId from ContentDocumentLink where LinkedEntityId = '0031J00001DkSTHQA3'

ContentDocumentId field in ContentDocumentLink stores the file references associated with the parent entity. I have three files against this contact, and I see three records in the query output. Collect ‘ContentDocumentId’ attribute from the output.

ContentDocument stores the file relationships & references, while ContentVersion object stores the versions and the file content references. Initiate a get request to –

SELECT VersionData FROM ContentVersion WHERE (ContentDocumentId  = '0691J000004Md1DQAS' or ContentDocumentId  = '0691J000004Me8yQAC' or ContentDocumentId  = '0691J000004Me93QAC') AND IsLatest = true
sfdc rest api for attachments contentversion

You could click on the VersionData link to send another request to retrieve the actual file content. For e.g.:

https://crmcog-dev-ed.my.salesforce.com/services/data/v43.0/sobjects/ContentVersion/0681J000004RjdgQAC/VersionData

The data you is in base64 encoding and direct view may not make sense. Workbench lets you download the actual file if you had used the same get operation as above, but would need you to repeat the clicks for each file record.

Simple Javascript Utility to Get SFDC Files

The below script merges all the above operations. It is written in Javascript and tested in Windows using node.

/** 
 * Caution: shabby script ahead.
 *
 * Description: Get files from SFDC for given record ids. Simple script, almost no error handling.
 * This is a simple REST API call to demonstrate attachment retrieval from SFDC. It is not scalable, does not handle errors properly, and is not production-ready.
 * Author:      Prashanth Krishnamurthy, @crmcog
 * License:     MIT
**/

var fetch = require('cross-fetch');

const sec = {
    userName:"...@gmail.com",
    secPassToken:"...",
    client_id:"client_id",
    client_secret:"client_secret"
};

const recordId = ['0031J00001DkSTHQA3']; //contact id


const authUrl = "https://login.salesforce.com/services/oauth2/token?grant_type=password&client_id="+ sec.client_id + "&client_secret=" + sec.client_secret + "&username="+sec.userName+ "&password=" + sec.secPassToken;

// authenticate
fetch(authUrl, {method: "POST"})
    .then(data => {return data.json()})
    .then(res => {
        console.log("Authentication successful. access_token: " + res.access_token + '; instance_url:' + res.instance_url);
        //get contact
        sec.access_token = res.access_token;
        sec.instance_url = res.instance_url;
        getContactAttachments(sec, recordId);
        return res;
    })
    .catch(error => {console.error("Authentication error: " + error)});


function getContactAttachments(sec, recordId){
    try{ 
        
        // create sub directories to store files
        const fs = require('fs');
        const dirTimePrefix = new Date().getTime();
        const dirName= __dirname + "\\sfdc-files-" + dirTimePrefix;

        if (!fs.existsSync(dirName)) fs.mkdirSync(dirName); 
        console.log("created/found directory: " + dirName);

        console.log('get attachment referencess associated with contact..');
        const getURL = sec.instance_url + "/services/data/v43.0/query?q=";
        sec.authHeader = {
            method: "GET",
            headers:{
                'Authorization':'Bearer ' + sec.access_token
            }
        }

        //https://<instance_url>/services/data/v43.0/query?q=select id, LinkedEntityId,ContentDocumentId from ContentDocumentLink where LinkedEntityId = '0031J00001DkSTHQA3'
        fetch(getURL+"select id, LinkedEntityId,ContentDocumentId from ContentDocumentLink where LinkedEntityId='" + recordId[0] + "'", sec.authHeader)
        .then(data => {return data.json()})
        .then(res => {
            console.log("Contact '" + recordId[0] + "' has " + res.totalSize + " attachments");
            if (parseInt(res.totalSize, 10) > 0){

                //get all ContentDocumentIds in the result. This will have all the attachment references from ContentDocumentLink
                var contentDocumentIds = res.records.map(docId => "ContentDocumentId = '"+ docId.ContentDocumentId + "'");

                //https://<instance_url>/services/data/v43.0/query?q=SELECT VersionData FROM ContentVersion WHERE (ContentDocumentId  = '0691J000004Md1DQAS' or ContentDocumentId  = '0691J000004Me8yQAC' or ContentDocumentId  = '0691J000004Me93QAC') AND IsLatest = true
                fetch(getURL + "SELECT Id, Title, FileExtension, VersionData FROM ContentVersion WHERE (" + contentDocumentIds.join(' OR ') + ") AND IsLatest = true", sec.authHeader)
                .then(dataVersion => {return dataVersion.json()})
                .then(resVersion => {
                    console.log("Validating attachments.. found " + resVersion.totalSize + " attachments. Fetching them now..");
                    if (parseInt(resVersion.totalSize, 10) > 0){
                        var contentVersions = resVersion.records.map(contentVersion => dirName + "\\" + contentVersion.Title + "_" +contentVersion.Id + "." + contentVersion.FileExtension + "/\\/\\" + contentVersion.VersionData);
                        contentVersions.map(contentVersion => {getAttachment(sec, contentVersion)});
                    }
                })
                .catch(error => {console.error("getVersionData fetch error: " + error)});

    
            }
        })
        .catch(error => {console.error("getContactAttachments fetch error: " + error)});

    }

    catch(e){
        console.error("getContactAttachments error: " + e.toString());
    }

}

function getAttachment(sec, contentVersion){
    fileURL = contentVersion.split("/\\/\\");
    console.log('getting attachment ..' + sec.instance_url + contentVersion.split("/\\/\\")[1]);
    fetch(sec.instance_url + contentVersion.split("/\\/\\")[1], sec.authHeader)
    .then(dataAttach => {return dataAttach.buffer()})      
    .then(fileBuffer => {
        var fs = require ('fs');
        fs.writeFileSync(contentVersion.split("/\\/\\")[0], fileBuffer);
        console.log("file saved.. " + contentVersion.split("/\\/\\")[0]);
    })            
    .catch(error => {console.error("getAttachment fetch error: " + error)});

}

How to use the script?

  1. Install node if you don’t have that on your computer (no node? what kind of specimen are you?)
  2. Install cross-fetch that allows you to use fetch for API calls.
    npm i -g cross-fetch

    You could do this with XMLHttpRequest, but where’s the fun in that

  3. Ensure your NODE_PATH refers to global modules. The simplest way to do that is to edit ‘Environment Variables’ and set NODE_PATH to “%AppData%\npm\node_modules”
  4. Copy/paste the above code in get-sfdc-files-contact.js
  5. Open command prompt and execute “node get-sfdc-files-contact.js”
  6. The script will create a sub-directory and store all files with name as “<fileId> + <SFDCFileName>”

Found an error? Submit a pull request on GitHub.