How to create, store and use REST API tokens in Salesforce Marketing Cloud

How to create, store and use REST API tokens in Salesforce Marketing Cloud

This article showcases how to create, store and use REST API tokens in Salesforce Marketing Cloud for performing REST API requests between Cloud Pages and Code Resources.

REST API problems

REST API server-to-server integrations are our bread and butter when it comes to interacting with Marketing Cloud applications, such as Content Builder, Email Studio and others.

All we have to do is to generate an access token and use it to send an HTTP request to perform an action (POST, PUT, DELETE…) according to the API specifications.

It’s an amazing feature that opens many doors but unfortunately it’s not perfect.

Problem #1:

Unlike many other RESTful APIs, the one in Marketing Cloud doesn’t return a proper error message when something goes wrong with the access token.

In fact, there is no difference in the error message between a wrong, empty or expired access token:

The remote server returned an error: (401) Unauthorized.

This means that there is no way for us to know why a REST API request failed, except that something might be wrong with the token.

Problem #2:

There is no way to automatically refresh the token when it expires, which means that if we use the same token for about 20 minutes, our requests will start to return the error 401.

This means that we’ll need to regenerate the new token ourselves, when the request returns 401, hoping that the error occurs because it’s expired and not for any other reason.

Problem #3:

There is no out-of-the-box solution for storing a valid REST API token in Marketing Cloud.

Which means that we have no other choice but to waste 1 API request every time the token expires and to generate a new token for each API integration.

Why should we care?

Did you know that according to your contract with Salesforce, you only have a limited number of REST API requests per year?

Therefore, it’s very important to find a way to manage REST API requests in the most efficient way possible.

Here is what the documentation states about this subject:

Do not request a new access token for every API call you make – each access token is good for 20 minutes and is reusable. Making two API calls for every one operation is inefficient and causes throttling

The official documentation

How do we proceed?

In order to increase the efficiency of REST API integrations, we need to stop wasting unnecessary API requests.

Each token is valid for 20 minutes and can be used by multiple integrations at the same time.

All we have to do is to store the token along with its expiration date in a Data Extension and just query it later on whenever we need a valid token.

Sounds easy, but what about the data protection? The Data Extension records can be easily accessed by a large number of Marketing Cloud roles.

Your application must extract the access token and store it safely. Protect the access token as you would protect user credentials.

The official documentation

There is of course a secret method of hiding the Data Extension from the user interface, but it can still be accessed through AMPscript and server-side JavaScript.

In this case, EncryptSymmetric function is our best solution and we’ll need it to encrypt the token in the Data Extension.

Now let’s see how to put all of this together, shall we?

Before we start

Before we can start building our code, we need to create a Data Extension and a combination of 3 encryption keys.

The Data Extension in my example will be named “REST Tokens” and will have the following structure:

Field namePrimary KeyTypeLength
NameYesText50
access_tokenNoText4000
rest_instance_urlNoText4000
ExpirationDateNoDate

As for the encryption keys, please give them the following names:

  • pwd” for the Symmetric Key
  • slt” for the Salt
  • vec” for the Initialization Vector

In case you have no idea how to create encryption keys, please refer to the official documentation.

Make every request count

At first, let’s examine the code structure we are going to be using:

<script runat='server'>

    Platform.Load('core', '1');

    var config = {
        endpoint: "https://{{ YOUR DOMAIN }}.auth.marketingcloudapis.com/v2/token",
        credentials: {
            "{{ BU ID }}": {
                "grant_type": "client_credentials",
                "client_id": "{{ CLIENT ID }}",
                "client_secret": "{{ CLIENT SECRET }}"
            }
        },
        storage: {
            de: "REST_Tokens",
            name: "DEMO"
        },
        bu: Platform.Function.AuthenticatedMemberID(),
        localDate: DateTime.SystemDateToLocalDate(Now())
    }

    try {

        /// YOUR CODE

    } catch(error) {
		
        Write(Stringify(error));
		
    }
	
	/// YOUR FUNCTIONS

</script>

This is a classic method of structuring a server-side JavaScript code: it includes a config object that will hold all the static data and a Try…Catch function for an effective error handling.

Now, let’s build some functions to perform the different steps of our code.

Retrieve the token

This function checks the Data Extension for an existing and unexpired token.

If the token exists, it decrypts the token and returns it.

If the token doesn’t exist, it sends an API request to generate the token using a second function, then encrypts the token, before storing it in the Data Extension using a third function..

function retrieveToken() {

	var request = Platform.Function.LookupRows(config.storage.de, "Name", config.storage.name);

	var result = request[0];

	if(result != null && (new Date(config.localDate) < new Date(result.ExpirationDate))) {

		return {
			"token": decryptSymmetric(result.access_token),
			"url": decryptSymmetric(result.rest_instance_url),
			"expires": result.ExpirationDate
		}

	} else {

		var result = requestToken();

		var upsert = storeToken(result);

		if(upsert > 0) {

			return result;

		} else {

			throw "Token not saved"

		}
	}
}

Note how it compares the retrieved expiration date to the current date to figure out that the token is not expired.

Request the token

This function performs a REST API request to generate the token and returns it along with the expiration date.

function requestToken() {

    var request = HTTP.Post(config.endpoint, "application/json", Stringify(config.credentials[config.bu]));

    if (request.StatusCode == 200) {

        var result = Platform.Function.ParseJSON(request.Response[0]);

        var parsedDate = new Date(config.localDate);

        var expirationDate = new Date(parsedDate.getTime() + (result.expires_in * 1000));

        return {
            "token": result.access_token,
            "url": result.rest_instance_url,
            "expires": expirationDate
        }

    } else {
        
        throw "Couldn't request the token. Status: " + request.StatusCode;
    
    }
}

Store the token

Once the token is generated, it can be safely stored in a Data Extension, by encrypting the data.

function storeToken(result) {

    var rows = Platform.Function.UpsertData(
        config.storage.de,
        ["Name"], [config.storage.name],
        ["access_token", "rest_instance_url", "ExpirationDate"],
        [encryptSymmetric(result.token), encryptSymmetric(result.url), result.expires]
    );

    if(rows > 0) {
        
        return rows; 
        
    } else {
        
        throw "Token storage failed"
        
    }
}

Encrypt/Decrypt functions

These are some simple utility functions that use the SSJS enhancement method discussed in my previous article to encrypt or decrypt anything in Salesforce Marketing Cloud.

function decryptSymmetric(str) {

    Variable.SetValue("@ToDecrypt", str)

    var scr = "";
        scr += "\%\%[";
        scr += "SET @Decrypted = DecryptSymmetric(@ToDecrypt, 'AES', 'pwd', @null, 'slt', @null, 'vec', @null)";
        scr += "Output(Concat(@Decrypted))";
        scr += "]\%\%";

    return Platform.Function.TreatAsContent(scr);
}

function encryptSymmetric(str) {

    Variable.SetValue("@ToEncrypt", str)

    var scr = "";
        scr += "\%\%[";
        scr += "SET @Encrypted = EncryptSymmetric(@ToEncrypt, 'AES', 'pwd', @null, 'slt', @null, 'vec', @null)";
        scr += "Output(Concat(@Encrypted))";
        scr += "]\%\%";

    return Platform.Function.TreatAsContent(scr);
}

Full code

And finally, let’s put everything together to see how it works.

<script runat='server'>

    Platform.Load('core', '1');

    var config = {
        endpoint: "https://{{ YOUR DOMAIN }}.auth.marketingcloudapis.com/v2/token",
        credentials: {
            "{{ BU ID }}": {
                "grant_type": "client_credentials",
                "client_id": "{{ CLIENT ID }}",
                "client_secret": "{{ CLIENT SECRET }}"
            }
        },
        storage: {
            de: "REST_Tokens",
            name: "DEMO"
        },
        bu: Platform.Function.AuthenticatedMemberID(),
        localDate: DateTime.SystemDateToLocalDate(Now())
    }

    try {

        var auth = retrieveToken();

        Write(Stringify(auth));

    } catch(error) {
		
        Write(Stringify(error));
		
    }

    function requestToken() {

        var request = HTTP.Post(config.endpoint, "application/json", Stringify(config.credentials[config.bu]));

        if (request.StatusCode == 200) {

            var result = Platform.Function.ParseJSON(request.Response[0]);

            var parsedDate = new Date(config.localDate);

            var expirationDate = new Date(parsedDate.getTime() + (result.expires_in * 1000));

            return {
                "token": result.access_token,
                "url": result.rest_instance_url,
                "expires": expirationDate
            }

        } else {
			
            throw "Couldn't request the token. Status: " + request.StatusCode;
        
		}
    }

    function retrieveToken() {

        var request = Platform.Function.LookupRows(config.storage.de, "Name", config.storage.name);

        var result = request[0];

        if(result != null && (new Date(config.localDate) < new Date(result.ExpirationDate))) {

            return {
                "token": decryptSymmetric(result.access_token),
                "url": decryptSymmetric(result.rest_instance_url),
                "expires": result.ExpirationDate
            }

        } else {

            var result = requestToken();

            var upsert = storeToken(result);

            if(upsert > 0) {
				
                return result;
				
            } else {
				
                throw "Token not saved"
				
            }
        }
    }

    function storeToken(result) {

        var rows = Platform.Function.UpsertData(
            config.storage.de,
            ["Name"], [config.storage.name],
            ["access_token", "rest_instance_url", "ExpirationDate"],
            [encryptSymmetric(result.token), encryptSymmetric(result.url), result.expires]
        );

        if(rows > 0) {
			
            return rows; 
			
        } else {
			
            throw "Token storage failed"
			
        }
    }
    
    function decryptSymmetric(str) {

        Variable.SetValue("@ToDecrypt", str)

        var scr = "";
            scr += "\%\%[";
            scr += "SET @Decrypted = DecryptSymmetric(@ToDecrypt, 'AES', 'pwd', @null, 'slt', @null, 'vec', @null)";
            scr += "Output(Concat(@Decrypted))";
            scr += "]\%\%";

        return Platform.Function.TreatAsContent(scr);
    }

    function encryptSymmetric(str) {

        Variable.SetValue("@ToEncrypt", str)

        var scr = "";
            scr += "\%\%[";
            scr += "SET @Encrypted = EncryptSymmetric(@ToEncrypt, 'AES', 'pwd', @null, 'slt', @null, 'vec', @null)";
            scr += "Output(Concat(@Encrypted))";
            scr += "]\%\%";

        return Platform.Function.TreatAsContent(scr);
    }
</script>

Considerations

Although this method will work 99.9% of the time, there could be some rare occasions when the token expires just at the moment after it was retrieved from the Data Extension.

This eventuality is not covered in this article, as we are not making any API requests just yet, but consider re-triggering the token generation process in case your REST API call returns the status 401.

Final notes

Please keep in mind that this method is only valid when performing the REST API requests within Marketing Cloud.

If you are making these requests from an external system, a similar solution will need to be developed for managing the tokens.

Have I missed anything?

Please poke me with a sharp comment below or use the contact form.

server-side Javascript
Up Next:

How to use AMPscript to enhance server-side JavaScript in Salesforce Marketing Cloud

How to use AMPscript to enhance server-side JavaScript in Salesforce Marketing Cloud