Exception handling in Salesforce Marketing Cloud CloudPages

Errors and exceptions are inevitable, no matter how defensively you code your CloudPage. There is always a possibility of a human error, for example if the person who creates and sends the email doesn’t pass all the parameters to your CloudPage correctly. There are also things that sometimes cannot be prevented, for example Marketing Cloud Connector getting disconnected while your script utilizes the Sales and Service Cloud AMPscript functions.

Although this solution has been mentioned multiple times in various articles on this and other blogs, I decided it needed it’s own article, as we can often see that Salesforce Marketing Cloud developers struggle to troubleshoot problems and errors on their CloudPages.

In one of my previous article, Exception handling in Salesforce Marketing Cloud emails, I have presented a way to log any errors evaluated by the RaiseError() function into a Data Extension. Today, I would like to show you a very quick and easy way to achieve this on a CloudPage.

Try/catch statement

The best way, not only to debug your CloudPages, but also to handle all exceptions, is to include a JavaScript Try/Catch statement in all your CloudPages, regardless of whether you are coding them using AMPscript or Server-Side JavaScript.

The try statement allows you to define a block of code to be tested for errors while it is being executed. The catch statement allows you to define a block of code to be executed, if an error occurs in the try block. Wrapping the whole code included in your CloudPage in a try/catch statement, will not only help you catch errors, regardless of in which part of the code they appear, but it will also prevent your subscribers from seeing the dreaded 500 error if something goes wrong. Instead, you will be able to still display your CloudPage properly with all it’s branding, and include either a generic or a personalized error message: [see code snippet]

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
{{your script goes here}}
<script runat="server">
}catch(e){
Write("Oops, something went wrong!<br>Please contact info@email.com so we can help you finalize your request.");
}
</script>

If you would like to see an example of a more complex script with the try/catch statement included, you can take a look at my Salesforce-Integrated Subscription and Profile Center script.

Log all caught errors

Now that we have a mechanism for catching errors on a CloudPage, it would be also good to have somewhere to store them, so that they can be reviewed on a regular basis. This can be easily achieved by creating a dedicated Data Extension and inserting a new row to that Data Extension every time an error is caught.

Here is an example of a Shared Data Extension created for error logging. It holds information about the Subscriber, MID, date of the event and the error message itself:

You can add other fields if needed, but remember to make them nullable – sometimes, depending on the error, some variables or personalization strings might not be available, and in that case you will have to do with just the date and the error message.

Now let’s add a script that will allow us to log data into the above Data Extension: [see code snippet]

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
{{your script goes here}}
<script runat="server">
}catch(e){
var errorMsg = Stringify(e.message)
Write("Oops, something went wrong!<br>Please contact info@email.com so we can help you finalize your request.");
Variable.SetValue("@err",errorMsg);
}
</script>
%%[
if not empty(@err) then
InsertData('ent.ErrorLogging','SubscriberKey',_SubscriberKey,'MID',memberid,'error',@err)
endif
]%%

The above is a mix of Server-Side JavaScript and AMPscript, which can be used to log data from the Personalization Strings, but depending on your use case, you could also log errors using just SSJS: [see code snippet]

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
{{your script goes here}}
<script runat="server">
}catch(e){
var errorMsg = Stringify(e.message)
Write("Oops, something went wrong!<br>Please contact info@email.com so we can help you finalize your request.");
var targetDE = 'xxxxxxx-xxxxxxx-xxxxxxxxx'; //pass external key of the target DE
var logDE = DataExtension.Init(targetDE);
logDE.Rows.Add({ Error: errorMsg });
}
</script>

Remember to always include exception handling in your emails and CloudPages, as this will help you maintain excellent reputation with your subscribers, while constantly improving the knowledge about your data and the quality of your code.


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Inventory of Data Extensions and their data sources

It’s very likely that at some point in your career you will inherit or take over a Salesforce Marketing Cloud instance that has been set up and used by someone else in the past. In case it hasn’t been well documented, it can be a struggle to understand all the data related processes and clean up all the unused Data Extensions.

Gregory Gifford has written a very useful blog post, where he explains how to create a Data Extension Inventory for your SFMC Business Unit. Using the code snippets he provided, you will be able to populate a complete list of all the Data Extensions in your Business Unit, along with their properties:

Source: https://gortonington.com/data-extension-inventory-for-your-sfmc-business-unit/

But where does the data in those Data Extensions come from? Of course, it won’t be possible to track everything – things like manual data imports, data uploaded through scripts or coming in through API won’t leave any trace in the system. Fortunately, we can track data-related activities that are scheduled in Automation Studio: Imports and Queries. We will use two SOAP API objects to obtain all the information we need: ImportDefinition and QueryDefinition.

But before we do that, let’s first create a Data Extension where we will populate the results of our calls. In the script, we are going to retrieve the following data:

  • Data Extension’s Name,
  • Data Extension’s External Key,
  • the name of the activity in Automation Studio which populates the data,
  • the type of the activity (Import/Query),
  • the description of the activity if one has been set up.

Here’s the Data Extension structure you will need:

Retrieve the ImportDefinition data

We will use WSProxy to retrieve information about Imports that have been set up and used in Automation Studio and to get information about Data Extensions they populate with data. The properties we will use in our script are the following:

  • Name – name of the Import Definition,
  • Description – description of the Import Definition,
  • ObjectID – ID of the Import Definition,
  • DestinationObject.ObjectID – the ID of the destination. In this context, this could be either a Data Extension or a List, so we will need to filter out Lists later on.

As you can see above, we can get only as much as the Data Extension’s ObjectID from the ImportDefinition object, so we will add another call to our script. We will call the DataExtension object to check the Name and the CustomerKey of that Data Extension using the ObjectID value to match it correctly.

Let’s combine all of the above together and add pagination, so that we are not limited by the fact that most API objects have a predefined batch size of 2500. Here’s the full function: [see code snippet]

function getImports(objectType, cols) {
var prox = new Script.Util.WSProxy(),
objectType = "ImportDefinition",
cols = ["DestinationObject.ObjectID", "Description", "Name", "ObjectID"],
moreData = true,
reqID = null,
numItems = 0;
while (moreData) {
moreData = false;
var data = reqID == null ? prox.retrieve(objectType, cols) : prox.getNextBatch(objectType, reqID);
if (data != null) {
moreData = data.HasMoreRows;
reqID = data.RequestID;
if (data && data.Results) {
for (var i = 0; i < data.Results.length; i++) {
if (data.Results[i].DestinationObject.ObjectID) {
var cols = ["Name", "CustomerKey", "ObjectID"];
var filter = {
Property: "ObjectID",
SimpleOperator: "equals",
Value: data.Results[i].DestinationObject.ObjectID
};
var deName = prox.retrieve("DataExtension", cols, filter);
if (deName.Results[0].Name) {
logDE.Rows.Add({
DEName: deName.Results[0].Name,
DEKey: deName.Results[0].CustomerKey,
ActivityName: data.Results[i].Name,
ActivityType: "Import",
ActivityDescription: data.Results[i].Description
});
}
}
numItems++;
}
}
}
}
Write("<br />" + numItems + " total " + objectType + "<br>");
}
view raw get-imports.js hosted with ❤ by GitHub

Retrieve the QueryDefinition data

The QueryDefinition object is a bit easier to work with because SQL queries in Salesforce Marketing Cloud only have one type of destination: a Data Extension. That’s why the QueryDefinition object will be able to return everything we need in one call. The properties we will use in our script are the following:

  • Name – name of the Query Definition,
  • Description – description of the Query Definition,
  • DataExtensionTarget.Name – name of the target Data Extension,
  • DataExtensionTarget.CustomerKey – external key of the target Data Extension.

Again, we will add pagination to make sure we retrieve everything. Here’s the full function: [see code snippet]

function getQueries(objectType, cols) {
var prox = new Script.Util.WSProxy(),
objectType = "QueryDefinition",
cols = ["Name", "Description", "ObjectID", "DataExtensionTarget.Name", "DataExtensionTarget.CustomerKey"],
moreData = true,
reqID = null,
numItems = 0;
while (moreData) {
moreData = false;
var data = reqID == null ? prox.retrieve(objectType, cols) : prox.getNextBatch(objectType, reqID);
if (data != null) {
moreData = data.HasMoreRows;
reqID = data.RequestID;
if (data && data.Results) {
for (var i = 0; i < data.Results.length; i++) {
logDE.Rows.Add({
DEName: data.Results[i].DataExtensionTarget.Name,
DEKey: data.Results[i].DataExtensionTarget.CustomerKey,
ActivityName: data.Results[i].Name,
ActivityType: "Query",
ActivityDescription: data.Results[i].Description
});
numItems++;
}
}
}
}
Write("<br />" + numItems + " total " + objectType + "<br>");
}
view raw get-queries.js hosted with ❤ by GitHub

The full script

Now let’s combine everything together and run our script – you can either run it on a CloudPage, or in Automation Studio. Note, that depending on the volume of activities in your account, this script can take even several minutes to resolve. All you will need to do with below script is to provide the External Key of the Data Extension you created earlier, which will hold the results of our calls: [see code snippet]

<script runat="server">
Platform.Load("Core", "1");
try {
//initiate WSProxy
var prox = new Script.Util.WSProxy();
//initiate data extension
var targetDE = 'xxxxxxx-xxxxxxx-xxxxxxxxx'; //pass external key of the target DE
var logDE = DataExtension.Init(targetDE);
function getImports(objectType, cols) {
var objectType = "ImportDefinition",
cols = ["DestinationObject.ObjectID", "Description", "Name", "ObjectID"],
moreData = true,
reqID = null,
numItems = 0;
while (moreData) {
moreData = false;
var data = reqID == null ? prox.retrieve(objectType, cols) : prox.getNextBatch(objectType, reqID);
if (data != null) {
moreData = data.HasMoreRows;
reqID = data.RequestID;
if (data && data.Results) {
for (var i = 0; i < data.Results.length; i++) {
if (data.Results[i].DestinationObject.ObjectID) {
var cols = ["Name", "CustomerKey", "ObjectID"];
var filter = {
Property: "ObjectID",
SimpleOperator: "equals",
Value: data.Results[i].DestinationObject.ObjectID
};
var deName = prox.retrieve("DataExtension", cols, filter);
if (deName.Results[0].Name) {
logDE.Rows.Add({
DEName: deName.Results[0].Name,
DEKey: deName.Results[0].CustomerKey,
ActivityName: data.Results[i].Name,
ActivityType: "Import",
ActivityDescription: data.Results[i].Description
});
}
}
numItems++;
}
}
}
}
Write("<br />" + numItems + " total " + objectType + "<br>");
}
function getQueries(objectType, cols) {
var objectType = "QueryDefinition",
cols = ["Name", "Description", "ObjectID", "DataExtensionTarget.Name", "DataExtensionTarget.CustomerKey"],
moreData = true,
reqID = null,
numItems = 0;
while (moreData) {
moreData = false;
var data = reqID == null ? prox.retrieve(objectType, cols) : prox.getNextBatch(objectType, reqID);
if (data != null) {
moreData = data.HasMoreRows;
reqID = data.RequestID;
if (data && data.Results) {
for (var i = 0; i < data.Results.length; i++) {
logDE.Rows.Add({
DEName: data.Results[i].DataExtensionTarget.Name,
DEKey: data.Results[i].DataExtensionTarget.CustomerKey,
ActivityName: data.Results[i].Name,
ActivityType: "Query",
ActivityDescription: data.Results[i].Description
});
numItems++;
}
}
}
}
Write("<br />" + numItems + " total " + objectType + "<br>");
}
getImports(objectType, cols)
getQueries(objectType, cols)
} catch (e) {
Write(Stringify(e));
}
</script>

Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Two alternative ways of developing and testing CloudPages

As a Salesforce Marketing Cloud developer, you have certainly been caught up in this loop: Develop the CloudPage > Save and Publish > Wait 2, 3 or even 5 minutes > Check the results. Moreover, if you are not debugging correctly, you can lose a lot of time between looking for the problem and waiting for your updates to propagate on your CloudPage. I must take this opportunity to invite you to check out Zuzanna’s article about debugging AMPScript in CloudPages, it is very helpful and well explained.

In this article, we will be covering two ways of testing your code on a CloudPage without having to wait for the propagation of changes every time we save and publish our work. These two ways are probably well know by experimented Salesforce Marketing Cloud developers, or by those who were fortunate enough to stumble upon some answers regarding this problem on Salesforce Stack Exchange.

Create an external web page

If you already have a web server, that is great. You can use it to create your web pages. However, if you are on a budget, or you just do not want to spend money on a hosting service, don’t worry, I have done the heavy lifting for you.

We are going to use awardspace.com, an ad-free hosting which offers:

  • Free subdomain
  • 1GB disk space
  • Up to 4 websites (websites and not pages)

Those features are more than enough for our tests. Especially because we can use one or two pages only and overwrite their content every time we need to develop a new CloudPage.

Step 1: Create a freehosting account

Go to awardspace.com and click on FreeHosting on the homepage:

Create an account using one of the available options:

Step 2: Creating a free subdomain

Go to Hosting Tools > Domain Manager. Select “Create a free subdomain”, fill in the subdomain’s name, and click Create:

Step 3: Creating the web page

Go to Hosting Tools > File Manager. Double click on your subdomain’s folder:

Click on Create and check Create file. Give it a name like sfmc.html for example.

Step 4: add content and access the web page

Congrats, we have successfully created the web page. Double click on it to add the content. Click save when done editing The web page’s URL is as shown below without the /home/www/ part.

Call the page from within Salesforce Marketing Cloud

We are going to use two AMPScript functions to access our external page from within our CloudPage.

  • HTTPGet: Returns the content from our external web page. This function will only take our web page URL as a parameter.
  • TreatAsContent: Treats the content returned by the HTTPGet function as it came from a content area. This way, we make sure the AMPScript code in our web page gets evaluated.

For example, our CloudPage on Marketing Cloud should contain something like: [see code snippet]

Each time we need to update the code, we just have to update it on the external web page. No need to save and publish our CloudPage as its content will remain the same.  

This way, we are avoiding the wait time between the publishing and the propagation of our changes on the servers when using a traditional development method.

Content Block By Key / Name / Id

The second method is using one of the three functions above. These functions return the content stored in the specified Content Block.

First, we will start by creating our Content Block. We need to go to Content Builder > Create > Content Blocks > Code Snippet.

I highly suggest using a Code Snippet instead of an HTML Content Block in order to avoid Content Builder’s editor from truncating parts of our HTML code.

Once we have created our Content Block and added our content in it. Time for getting it’s Key, Id or Name depending on which function we are using. We can find it by clicking on the drop-down icon and select Properties.

Now, we can head in to our CloudPage and add the code below: [see code snippet]

%%=ContentBlockbyKey("4bd9cced-f9c5-49fe-927a-xxxxxxx")=%%

It pretty simple and straightforward. The code uses ContentBlockByKey to get our Content Block’s content. This is a onetime operation. Every time we need to update our code, we will only have to update it on the Content Block. This way, the updates are instantly live and we do not have to wait several minutes for the propagation on the servers.

Once we are done testing, we can copy the code on the external page or on the Content Block to our main CloudPage in Marketing Cloud and publish it.


About the author

Rachid Mamai is a SFMC geek and a Digital Marketing enthusiast living in France. To get in touch with Rachid, visit his LinkedIn.

Trigger SMS text messages using Server-Side JavaScript

In order to be able to programmatically trigger text messages, you will need to have MobileConnect enabled in Marketing Cloud’s Mobile Studio. You will also need to be able to create an installed package in your Marketing Cloud account to interact with Salesforce Marketing Cloud APIs. If you have both in place, we can start by creating a new SMS message.

Create a new message in MobileConnect

Go to Mobile Studio > Mobile Connect and click on Create Message. Choose Outbound and click on Next. In Message Setup, choose a Name, Short/Long Code to be used, From Name and choose API Trigger as the Send Method. Click on Next, type the Outbound Message text and choose the Next Keyword if needed. To finalize, click on Activate. Before you confirm, note the API Key displayed in the pop-up:

The message will now be visible in the Overview screen – make sure it’s status is Active/Scheduled.

Trigger the message using API

In order to trigger a text message, we will have to interact with Salesforce Marketing Cloud’s REST API using the /sms/v1/messageContact/{id}/send route, but before we do that, we will need to authenticate.

Authenticate using Server-Side JavaScript

Let’s start by installing a new, or identifying an existing installed package in your Marketing Cloud account. Note the Client Id and Client Secret as we will need them to authenticate our API request. Make sure that SMS is enabled in the scope of the package you are using:

Depending on the type of the package installed (v2 – enhanced functionality or v1 – legacy functionality), use one of the following code snippets to cover the authentication part of the script.

v1: [see code snippet on Github]

v2: [see code snippet on Github]

<script runat="server">
Platform.Load("Core","1.1.1");
var authEndpoint = 'https://mcxxxxxxxxxxxxxxxxxxxxxxxxx.auth.marketingcloudapis.com&#39;;
var payload = {
client_id: "insert Client Id",
client_secret: "insert Client Secret",
grant_type: "client_credentials"
};
var url = authEndpoint + '/v2/token';
var contentType = 'application/json';
try {
var accessTokenRequest = HTTP.Post(url, contentType, Stringify(payload));
if(accessTokenRequest.StatusCode == 200) {
var tokenResponse = Platform.Function.ParseJSON(accessTokenRequest.Response[0]);
var accessToken = tokenResponse.access_token;
}
} catch (error) {
Write(Stringify(error));
}
</script>

Make a messageContact API Request

Below is an example messageContact API Request. The phone number passed in the payload must use the correct format for the designated country code. For example, a mobile number from the United States must include the numerical country code 1 and the area code, eg: 13175551212. [see code snippet on Github]

Host: https://YOUR_SUBDOMAIN.rest.marketingcloudapis.com
POST /sms/v1/messageContact/MzA6Nzg6MA/send
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"mobileNumbers": [
"13175551212"
],
"Subscribe": true,
"Resubscribe": true,
"keyword": "JOINSMS"
}
view raw messageContact hosted with ❤ by GitHub

To ensure that the mobile number exists for the contact and that the contact subscribed to the specified keyword on your short code, set the Subscribe and Resubscribe values to true and specify the keyword parameter. If you’re not sure how to work with keywords, check out this help document: Keywords and Codes.

If the request is valid, the API returns a token that can be used to make a follow-up call to check the status of the request: [see code snippet on Github]

HTTP/1.1 202 Accepted
{
"tokenId": "c21NCNSDN2sMMWM2miosdjEHH",
}

Now, let’s combine the first script snippet we used to authenticate with the messageContact payload and make a POST request. Below will work with the v1 legacy package: [see code snippet on Github]

<script runat="server">
Platform.Load("Core","1.1.1");
try {
//API authentication
var authEndpoint = ""; //provide authentication endpoint
var payload = {
clientId: "", //provide Client Id
clientSecret: "" //provide Client Secret
};
var url = authEndpoint + "/v1/requestToken";
var contentType = "application/json";
var accessTokenRequest = HTTP.Post(url, contentType, Stringify(payload));
if(accessTokenRequest.StatusCode == 200) {
var tokenResponse = Platform.Function.ParseJSON(accessTokenRequest.Response[0]);
var accessToken = tokenResponse.accessToken;
}
//make a call using the messageContact route
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var smsEndpoint = ""; //provide rest endpoint
var smsPayload = {
"mobileNumbers": [
"" //pass phone number
],
"Subscribe": true,
"Resubscribe": true,
"keyword": "" //provide KEYWORD
};
var smsUrl = smsEndpoint + "/sms/v1/messageContact/xxxxxxxxx/send"; //provide message API key
var sendSMS = HTTP.Post(smsUrl, contentType, Stringify(smsPayload), headerNames, headerValues);
} catch (error) {
Write(Stringify(error));
}
</script>

In the above script, you will need to provide your endpoints, ClientId and ClientSecret (all three can be found in Setup > Apps > Installed Packages). You will also need to insert the message Api Key, the keyword and pass the recipient’s phone number.

Last but not least, always remember to put security measures into practice when setting up this kind of functionality on a CloudPage to prevent your ClientId and ClientSecret from being exposed.


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Loops in AMPscript and Server-Side JavaScript

In programming languages, loops are a basic, yet a very powerful and indispensable concept. They shorten the code and reduce the need to repeat tasks. A loop is a set of instructions to be executed until a certain condition is met. In this article, we will focus on the most fundamental kind of loop, the for loop, and it’s usage in AMPscript and Server-Side JavaScript.

For loops in AMPscript

NOTE: In this article, we will focus on understanding the basic concepts of working with loops in Salesforce Marketing Cloud. For a strictly technical specification, defer to the following article: AMPscript Process Loops.

A for loop, which is actually the only loop available in AMPscript, lets you execute the same script repeatedly until an ending condition is met.

Let’s use the following example: we have a Data Extension with customer IDs and their ordered items. We want to use that dataset to send an email to each customer, listing the items each of them ordered.

Here’s our Data Extension called Orders:

CustomerIdOrderIdOrderItem
0031t000005D98UAAS4632Women’s Marine Hoodie
0031t000005D98UAAS4632Glass Love Bottle – 16.9 oz
0031t000005D98UAAS4632Eco Ballpoint Pen
0031t00000ZnTENAA33686Men’s Marine Hoodie
0031t00000YEM4ZAAX7354Leather Hampton Watch

In our example, the script contained within the loop would do the following: display a bullet point and display OrderItem name, and repeat the same action until all the OrderItems for a relevant Customer were displayed.

The most important element of any loop is the counter. The counter will define how many times the script in the loop should be executed. In order to define the counter, we must first set the criteria to find all the matching results in our Data Extension. We will use the LookupRows function to look up all rows, that match the Id of the Customer, that we want to send the email to. Then, we will use the RowCount function to determine how many results were found. The outcome of the RowCount function will be our counter and will decide how many times the script inside the loop will be repeated. Here’s the first part of our script, where we count how many records match the criteria: [click here to see the code on Github]

var @rows, @row, @rowCount, @subKey
set @subKey = _subscriberkey
set @rows = LookupRows("Orders","CustomerId", @subKey)
set @rowCount = rowcount(@rows)

Now let’s think through the loop logic. This is the verbal description of the loop that we would like to build:

Start at 1 and repeat the following in a loop until you reach the number defined in the RowCount variable: display the current index and the name of the OrderItem found in the referring row from the results of the LookupRows function. Increase the index by one and repeat the process. Stop looping once the index reaches the number defined in the counter.

Here’s how this looks in AMPscript: [click here to see the code on Github]

%%[ var @rows, @row, @rowCount, @subKey, @counter, @orderItem
set @subKey = _subscriberkey
set @rows = LookupRows("Orders","CustomerId", @subKey)
set @rowCount = rowcount(@rows)
for @counter = 1 to @rowCount do /* repeat in a loop for as many times as defined in rowCount */
set @row = row(@rows, @counter) /* get row based on counter */
set @orderItem = field(@row,"OrderItem")
]%%
<br>
Order item %%=v(@counter)=%%: %%=v(@orderItem)=%%<br>
%%[ next ]%%
view raw lookup-loop.amp hosted with ❤ by GitHub

You will notice the next keyword at the end of the loop – each time this keyword is reached, the system compares the current index to the value of the counter defined by the RowCount function. If the value is not equal to the end index, the loop will repeat until the end index is reached.

Here’s a full script that we can now use in an email, with added exception handling that will show “No items to display” in case the LookupRows function doesn’t find any matching records for a subscriber in the Orders Data Extension: [click here to see the code on Github]

<html>
<body>
Dear Customer,<br>
Here are the details of your order:<br>
%%[ var @rows, @row, @rowCount, @subKey, @counter, @orderItem
set @subKey = _subscriberkey
set @rows = LookupRows("Orders","CustomerId", @subKey)
set @rowCount = rowcount(@rows)
if @rowCount > 0 then
for @counter = 1 to @rowCount do
set @row = row(@rows, @counter) /* get row based on counter */
set @orderItem = field(@row,"OrderItem") ]%%
&#8226; Order item %%=v(@counter)=%%: %%=v(@orderItem)=%%<br>
%%[ next ]%%
%%[ else ]%%
No items to display
%%[ endif ]%%
<br>
We hope to see you again soon!
</body>
</html>
view raw ampscript-loop.html hosted with ❤ by GitHub

For loops in Server-Side Javascript

While JavaScript offers at least five different kinds of loops, and all of those can be used in Salesforce Marketing Cloud, in this article we will focus on the most basic one, the for loop.

We will create a similar solution to the one created in the AMPscript example above. We will use the same Data Exteniosn called Orders and we will use a loop to display all the OrderItems for a Customer on a CloudPage.

The first thing we have to do in order to interact with a data extension via server-side JavaScript is to initialize the Data Extension object. Then, we will use the Server-Side JavaScript Rows.Lookup function to find all rows that match the Id of the Customer. For the purpose of this tutorial, we will hardcode the CustomerId into the script: [click here to see the code on Github]

<script runat="server">
Platform.Load("core","1");
var custId = "0031t000005D98UAAS"
var OrdersDE = DataExtension.Init("Orders");
var rows = OrdersDE.Rows.Lookup(["CustomerId"], [custId]);
</script>

The above script will return an ArrayList, which is a collection of elements. Arrays use numbers to access its elements and the numbering always starts at zero:

Arrays in JavaScript are zero-based. This means that JavaScript starts counting from zero when it indexes an array. In other words, the index value of the first element in the array is “0” and the index value of the second element is “1”, the third element’s index value is “2”, and so on.

https://blog.kevinchisholm.com/javascript/javascript-array-length-always-one-higher/

In AMPscript we used the RowCount function to determine the counter for our loop. In JavaScript, we will use the Array length Property. Inside the loop, we will display the current index (i) and the name of the element (OrderItem) that has this index number in the ArrayList. Remember that we will start counting at “0”, not at “1” like we did in AMPscript.

Then, we will increase the index by one and repeat the process.

In JavaScript, the ++ notation is the increment operator, which means that i++ is exactly the same thing as i = i+1. The script will stop looping once the index reaches the number defined in the counter (rows.length). Here’s the script: [click here to see the code on Github]

<script runat="server">
Platform.Load("core","1");
var custId = "0031t000005D98UAAS"
var OrdersDE = DataExtension.Init("Orders");
var rows = OrdersDE.Rows.Lookup(["CustomerId"], [custId]);
if(rows.length >= 1) {
for (i = 0; i < rows.length; i++) {
OrderItem = rows[i]["OrderItem"];
Write("&#8226; OrderItem " + i + ":" + OrderItem + "<br>");
}
}
</script>

Here’s the full script that we can now use on a CloudPage, with added exception handling that will show “No items to display” in case there are no matching records found in the Orders Data Extension: [click here to see the code on Github]

Dear Customer,<br>
Here are the details of your order:<br>
<script runat="server">
Platform.Load("core","1");
var custId = "0031t000005D98UAAS"
var OrdersDE = DataExtension.Init("Orders");
var rows = OrdersDE.Rows.Lookup(["CustomerId"], [custId]);
if(rows.length >= 1) {
for (i = 0; i < rows.length; i++) {
OrderItem = rows[i]["OrderItem"];
Write("&#8226; OrderItem " + i + ":" + OrderItem + "<br>");
}
}
else {
Write("No items to display");
}
</script>
<br>
We hope to see you again soon!
view raw ssjs-loop.html hosted with ❤ by GitHub

The is how the results will be displayed on a CloudPage:

Additional resources

Here is a list of additional resources to help you understand both the general concept of loops, as well as using loops in AMPscript and Server-Side JavaScript:


Questions? Comments?

Leave a comment below or email me at zuzanna@sfmarketing.cloud.

Update multiple non-sendable Data Extensions to be sendable

In order to programmatically update the settings of an existing Data Extension, we will interact with Salesforce Marketing Cloud’s SOAP Web Services API using WSProxy. Code snippets in this article will show you how to update a Data Extension, so that a non-sendable Data Extension is turned into a sendable Data Extension, but you can modify the script to update other properties of a Data Extension, like retention settings or field properties.

DataExtension object

The DataExtension object represents a data extension within an account. In order to turn a non-sendable Data Extension into a sendable one, we will have to update three of the object’s properties:

  • IsSendable (xsd:boolean) – Indicates whether you can use a data extension as part of an audience for a message send.
  • SendableDataExtensionField (DataExtensionField) – Indicates the field within a sendable data extension to use as an address as part of a send. Possible values include SubscriberID, CustomerKey, or EmailAddress. The application uses this field to establish a data relationship between a value specified by the SendableSubscriberField property and a value within a sendable data extension.
  • SendableSubscriberField (Attribute) – Indicates field to use as sending address. The application uses this field to establish a data relationship between a subscriber field and a value specified by the SendableDataExtensionField property.

To put it in simple words, we need to set the IsSendable value to true and establish which field in the data extension relates to the Subscriber table. If you do it from the UI, you can for example set it to “ContactKey relates to Subscribers on Subscriber Key” or “EmailAddress relates to Subscribers on Subscriber Key”. Here, we will do exactly the same thing, using the following structure:

SendableDataExtensionField relates to subscribers on SendableSubscriberField

Update Data Extension properties via WSProxy

We will use the updateItem WSProxy function to interact with the DataExtension object. Below script will identify a Data Extension by it’s External Key (CustomerKey), set the IsSendable field to true and establish the following send relationship: “email relates to Subscribers on Subscriber Key”, where email is the Data Extension field of type EmailAddress.

The possible values of the SendableSubscriberField include “Subscriber Key” or “Email Address”, depending on whether the Subscriber Key business rule has been turned on. This is the tricky part of this script, as upon retrieving the properties of a sendable Data Extension, the results will return SendableSubscriberField.Name as “_SubscriberKey”, but you actually need to use “Subscriber Key” in the update call.

Here’s the script that will update a single Data Extension:

<script runat="server">
Platform.Load("core","1.1.5");
try{
var prox = new Script.Util.WSProxy();
var CustomerKey = "xxxxx-xxxx-xxxx-xxxx", //Insert DE External Key
IsSendable = "true",
SendableDataExtensionField = {"Name": "email", "DataType": "EmailAddress"}, //Provide the name of the field used to establish a data relationship (SendableDataExtensionField relates to subscribers on SendableSubscriberField)
SendableSubscriberField = {"Name": "Subscriber Key"}; //Use "Subscriber Key" or "Email Address" depending on whether the SUBSCRIBER_KEY business rule has been turned on.
var res = prox.updateItem("DataExtension", {"CustomerKey":CustomerKey, "IsSendable":IsSendable, "SendableDataExtensionField":SendableDataExtensionField, "SendableSubscriberField":SendableSubscriberField});
Write(Stringify(res));
}catch(e){
Write(Stringify(e));
}
</script>

Update multiple Data Extensions in one call

We have two possible ways to go when it comes to updating multiple Data Extensions in one call.

If you’re working with a set of Data Extensions that have an identical field name and data type assigned to the field used in the send relationship, for example email field of type EmailAddress, you can use a simple loop to iterate through an array of Data Extension External Keys:

<script runat="server">
Platform.Load("core","1.1.5");
try{
var prox = new Script.Util.WSProxy();
var CustomerKey = []; //Create an array withh DE External Keys
var IsSendable = "true",
SendableDataExtensionField = {"Name": "email", "DataType": "EmailAddress"}, //Provide the name of the field used to establish a data relationship (SendableDataExtensionField relates to subscribers on SendableSubscriberField)
SendableSubscriberField = {"Name": "Subscriber Key"}; //Use "Subscriber Key" or "Email Address" depending on whether the SUBSCRIBER_KEY business rule has been turned on.
for(var i=0; i< CustomerKey.length; i++) {
var res = prox.updateItem("DataExtension", {"CustomerKey":CustomerKey[i], "IsSendable":IsSendable, "SendableDataExtensionField":SendableDataExtensionField, "SendableSubscriberField":SendableSubscriberField});
Write(Stringify(res) + "<br>");
}
}catch(e){
Write(Stringify(e));
}
</script>

If the field names vary across the Data Extensions, it’s best to use the updateBatch WSProxy function and define properties of each Data Extension separately:

<script runat="server">
Platform.Load("core","1.1.5");
try{
var prox = new Script.Util.WSProxy();
var de1 = {
"CustomerKey":"xxxx-xxxx-xxxx-xxxx-xxxx-xxxx",
"IsSendable":"true",
"SendableDataExtensionField":
{
"Name": "email",
"DataType": "EmailAddress"
},
"SendableSubscriberField":
{
"Name": "Subscriber Key"
}
};
var de2 = {
"CustomerKey":"xxxx-xxxx-xxxx-xxxx-xxxx-xxxx",
"IsSendable":"true",
"IsTestable":"true",
"SendableDataExtensionField":
{
"Name": "emailaddress",
"DataType": "EmailAddress"
},
"SendableSubscriberField":
{
"Name": "Subscriber Key"
}
};
var res = prox.updateBatch("DataExtension", [de1, de2]);
Write(Stringify(res));
}catch(e){
Write(Stringify(e));
}
</script>

Additional resources

If you would like to learn more about using WSProxy and the DataExtension object, I highly recommend reading Gortonington’s article, WSProxy to copy a data extension.

Retrieve tracking data since account inception using WSProxy

Whether you’re building a data warehouse or are dealing with a legal compliance issue, access to historical tracking data from your Salesforce Marketing Cloud account using the out-of-the-box features is limited.

Data Views are powerful, but only allow you to query for up to six months of subscriber and tracking information.

Tracking extracts provide granular tracking data regarding several different aspects of email send jobs, such as clicks, bounces and unsubscribes. They are not time-restricted like Data Views, but if you want to get tracking data from a specific time range, the start and end dates can only be up to 30 days apart.

The third option to access historical data is to reach out to Salesforce support and ask them for a data export for a specific time range, but this service will come at a hefty price.

The method of retrieving tracking data using SOAP API and WSProxy also has some limitations, mostly related to performance when working with big data volumes, but it can be very useful in many cases where using Data Views or Tracking Extracts is not enough.

Retrieve Tracking Data with SOAP API

To retrieve tracking data using the SOAP API, we will call the SentEvent, OpenEvent, ClickEvent, BounceEvent and UnsubEvent objects, which will allow us to create a structure similar to the one we know from Data Views. Each of those objects has a persistent set of properties, which include: BatchID, ClientID, EventDate, SendID, SubscriberKey and TriggeredSendDefinitionObjectID.

Here is an example SOAP envelope that you could use in Postman to retrieve data from any of the tracking event objects:

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">Retrieve</a:Action>
<a:ReplyTo>
<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://{{your endpoint}}.soap.marketingcloudapis.com/Service.asmx</a:To>
<fueloauth xmlns="http://exacttarget.com">{{accessToken}}</fueloauth>
</s:Header>
<s:Body>
<RetrieveRequestMsg xmlns="http://exacttarget.com/wsdl/partnerAPI">
<RetrieveRequest>
<ObjectType>SentEvent</ObjectType>
<Properties>Client.ID</Properties>
<Properties>TriggeredSendDefinitionObjectID</Properties>
<Properties>SendID</Properties>
<Properties>SubscriberKey</Properties>
<Properties>EventDate</Properties>
<Properties>EventType</Properties>
<Properties>BatchID</Properties>
<Properties>ListID</Properties>
</RetrieveRequest>
</RetrieveRequestMsg>
</s:Body>
</s:Envelope>

Interact with SOAP API using WSProxy

To retrieve data from the tracking event objects using WSProxy, we will have to specify the properties to retrieve and perform the retrieve on each of the objects we are interested in. Here’s an example of retrieving data from the SentEvent object:

<script runat="server">
Platform.Load("Core","1");
var prox = new Script.Util.WSProxy();
var cols = [
"SendID",
"SubscriberKey",
"EventDate",
"EventType",
"TriggeredSendDefinitionObjectID",
"BatchID",
"Client.ID",
"ListID"
];
var data = prox.retrieve("SentEvent", cols);
Write(Stringify(data));
</script>

We can also add a filter to narrow down the results. You can filter the results by JobID, SubscriberKey or any other object property. Here, we are going to filter by EventDate, which will show us all the events that took place in the given timeframe:

<script runat="server">
Platform.Load("Core","1");
var prox = new Script.Util.WSProxy();
var cols = [
"SendID",
"SubscriberKey",
"EventDate",
"EventType",
"TriggeredSendDefinitionObjectID",
"BatchID",
"Client.ID",
"ListID"
];
var filter = {Property: "EventDate", SimpleOperator: "between", Value: ["2018-01-01T03:00:00.000Z", "2019-02-02T02:59:59.999Z"]};
var data = prox.retrieve("SentEvent", cols, filter);
Write(Stringify(data));
</script>

If you run the above script in an account that sends large volumes of emails, you will notice that it only pulls a part of the results and displays a status message: MoreDataAvailable. That’s because the SOAP API returns up to 2500 records at a time per retrieve call. To retrieve all available data for a given timeframe, we need to include pagination of retrieves in our script. Let’s also add an HTML table, so that data is displayed in a more accessible way:

<table border="1">
<tr>
<th>Ordinal</th>
<th>SendID</th>
<th>SubscriberKey</th>
<th>Eventhate</th>
<th>EventType</th>
<th>TriggeredSendDefinitionObjectID</th>
<th>BatchID</th>
<th>ClientID</th>
<th>ListID</th>
</tr>
<script runat="server">
Platform.Load("Core","1.1.1");
try{
var prox = new Script.Util.WSProxy(),
objectType = "SentEvent",
cols = [
"SendID",
"SubscriberKey",
"EventDate",
"EventType",
"TriggeredSendDefinitionObjectID",
"BatchID",
"Client.ID",
"ListID"
],
filter = {Property: "EventDate", SimpleOperator: "between", Value: ["2016-01-01T03:00:00.000Z", "2017-02-02T02:59:59.999Z"]},
moreData = true,
reqID = null,
numItems = 0;
while(moreData) {
moreData = false;
var data = reqID == null ?
prox.retrieve(objectType, cols, filter):
prox.getNextBatch(objectType, reqID);
if(data != null) {
moreData = data.HasMoreRows;
reqID = data.RequestID;
if(data && data.Results) {
for(var i=0; i< data.Results.length; i++) {
var j = +i +1
Write("<tr>");
Write("<td>" + j + "</td>");
Write("<td>" + data.Results[i].SendID + "</td>");
Write("<td>" + data.Results[i].SubscriberKey + "</td>");
Write("<td>" + data.Results[i].EventDate + "</td>");
Write("<td>" + data.Results[i].EventType + "</td>");
Write("<td>" + data.Results[i].TriggeredSendDefinitionObjectID + "</td>");
Write("<td>" + data.Results[i].BatchID + "</td>");
Write("<td>" + data.Results[i].Client.ID + "</td>");
Write("<td>" + data.Results[i].PartnerProperties[0].Value + "</td>");
Write("</tr>");
numItems++;
}
}
}
}
Platform.Response.Write("<b><br>" + numItems + " total " + objectType + ":<br><br></b>");
}catch(e){
Write(Stringify(e));
}
</script>
</table>

You can do the same with the remaining objects, here are the scripts:

Write retrieved data into a Data Extension

Although this will increase the processing time of the call, it will allow you to have the data ready for further processing without any additional preparations. Below script will create a new Data Extension called 01_SentEvent in your main Data Extensions folder and insert all the retrieved data.

<script runat="server">
Platform.Load("core","1.1");
try{
//Set time range of retrieved tracking data
var filter = {
Property: "EventDate",
SimpleOperator: "between",
Value: ["2016-01-01T00:00:00.000Z", "2017-10-30T00:00:00.000Z"]
};
//Set Data Extension properties
var fields = [
{ "Name" : "SendID", "FieldType" : "Number"},
{ "Name" : "SubscriberKey", "FieldType" : "Text", "MaxLength" : 50 },
{ "Name" : "EventDate", "FieldType" : "Date", "Ordinal" : 2 },
{ "Name" : "EventType", "FieldType" : "Text", "MaxLength" : 50 },
{ "Name" : "TriggeredSendDefinitionObjectID", "FieldType" : "Text", "MaxLength" : 50 },
{ "Name" : "BatchID", "FieldType" : "Number"},
{ "Name" : "ClientID", "FieldType" : "Number"},
{ "Name" : "ListID", "FieldType" : "Number"}
]
var SentEventDE = {
"CustomerKey" : "01_SentEvent",
"Name" : "01_SentEvent",
"Fields" : fields
};
//Create Data Extensions
var SentDE = DataExtension.Add(SentEventDE);
//Set columns to retrieve
var cols = ["SendID","SubscriberKey","EventDate","EventType","TriggeredSendDefinitionObjectID","BatchID","Client.ID","ListID"];
//Initiate data extensions
var logSentData = DataExtension.Init("01_SentEvent");
//Initiate WSProxy and set object properties
var prox = new Script.Util.WSProxy(),
objectType = "SentEvent",
moreData = true,
reqID = null,
numItems = 0;
//Retrieve data using pagination and insert data to SentEvent DE
while(moreData) {
moreData = false;
var data = reqID == null ?
prox.retrieve(objectType, cols, filter):
prox.getNextBatch(objectType, reqID);
if(data != null) {
moreData = data.HasMoreRows;
reqID = data.RequestID;
if(data && data.Results) {
for(var i=0; i< data.Results.length; i++) {
logSentData.Rows.Add({
SendID: data.Results[i].SendID,
SubscriberKey: data.Results[i].SubscriberKey,
EventDate: data.Results[i].EventDate,
EventType: data.Results[i].EventType,
TriggeredSendDefinitionObjectID: data.Results[i].TriggeredSendDefinitionObjectID,
BatchID: data.Results[i].BatchID,
ClientID: data.Results[i].Client.ID,
ListID: data.Results[i].PartnerProperties[0].Value
});
numItems++;
}
}
}
}
Platform.Response.Write("<br>Inserted " + numItems + " records<br>")
}catch(e){
Write(Stringify(e));
}
</script>

Here are the scripts that will do the same for the remaining objects:

Performance of the retrieve calls

The official Marketing Cloud SOAP API documentation states the following:

In most production implementations, data volume in the account requires you to include specific JobIDs in the filter criteria of the request. If requests that don’t specify a JobID time out during processing, add a JobID in the filter. If specifying a JobID is not possible, or if your implementation requires a broad range of JobIDs, use a data extract-based procedure instead.

Above is true if you are sending heavy volumes of emails each month, but you are not limited to filtering by JobID. You can include any of the object properties in filter criteria. You can, for example, retrieve all send and tracking history for a specific subscriber if you use the SubscriberKey in the filter: filter = {Property: "SubscriberKey", SimpleOperator: "equals", Value: "{{SubscriberKey}}"}

If you’re experiencing time outs when working with the script, try narrowing down the amount of retrieved data by using filters.

Security

Last but not least, always remember to put security measures into practice when setting up this kind of functionality on a CloudPage to prevent your data from being exposed in a breach. For the script to run and either return results onscreen or insert them into a Data Extension, you don’t have to actually publish the CloudPage – just press “Publish”, without further saving it. Or, to be completely safe – create a Script Activity in Automation Studio and run it from there.

Retrieve client IP address and geolocation in CloudPages

There are many reasons for checking the client IP address, most common include tracking and personalization. What can you find out about the visitors of a webpage from their IP address? You can identify their ISP, figure out approximately where they’re located and see how often they (or someone else sharing their router) visit your website.

In the context of CloudPages, we most often see IP tracking for personalization purposes. By identifying the visitor’s location, you can automatically display text in their local language and control what kind of content they see.

Identify client IP using AMPscript

The X-Forwarded-For (XFF) HTTP header is a standard header for identifying the originating IP address of a client connecting to a web server. You can easily access this header by using the AMPscript HTTPRequestHeader function, which will return a specified header from an HTTP request. Here’s how to retrieve an IP address on a CloudPage using AMPscript:

Your IP: %%=HTTPRequestHeader("X-Forwarded-For")=%%
view raw x-forwarded-for.html hosted with ❤ by GitHub

Identify client IP using SSJS

There are dedicated Server-Side JavaScript HTTP Properties Functions, that allow you to retrieve various types of HTTP Request object properties and platform application values. The client browser passes this information to the server during an HTTP interaction, so this object contains information regarding the browser and session. One of the available properties to use with the HTTP Request object is ClientIP, which returns the IP address of the requesting client as a string value. Here’s how to retrieve an IP address on a CloudPage using SSJS:

<script type="javascript" runat="server">
Platform.Load("core","1");
var ip = Platform.Request.ClientIP();
Write("Your IP: " + ip);
</script>
view raw clientIP.html hosted with ❤ by GitHub

This is the preferred way to retrieve the client IP, as Request.ClientIP() is a dedicated and supported function, while using the XFF HTTP header proved to be unreliable in the past.

HTTP Properties Functions allow you to retrieve other useful information, for example, browser metadata or the URL of the referring web address.

Discover the precise physical location of a given IP address

The functionality of identifying a physical location of a given IP address requires using a third party API service. There are many IP Geolocation API providers and most of them have a free plan available, as well as paid plans for bigger enterprises. My preferred one is ipify.org, which allows you to run up to 1000 queries per month for free. Once you register, you will obtain your personal apiKey, that will be used for making the calls:

<script type="javascript" runat="server">
Platform.Load("core","1");
var ip = Platform.Request.ClientIP();
var apiKey = '' //insert apiKey obtained from https://geo.ipify.org/subscriptions
var url = 'https://geo.ipify.org/api/v1?apiKey=&#39;;
var response = HTTP.Get(url + apiKey + ip);
Write("response.Status: " + response.Status + '<br />');
Write("response.Content: " + response.Content);
</script>

The response will contain information about the country, region, city, latitude, longitude, postal code, timezone and GeoNames Id. Additionally, it will also show autonomous system (AS) info if available.

{
"ip": "111.100.64.10",
"location": {
"country": "JP",
"region": "Saitama",
"city": "Saitama",
"lat": 35.867,
"lng": 139.65,
"postalCode": "337-0042",
"timezone": "+09:00",
"geonameId": 6940394
},
"as": {
"asn": 2516,
"name": "KDDI",
"route": "111.100.0.0/16",
"domain": "http://www.kddi.com/english/index.html",
"type": "NSP"
},
"isp": "Kddi Corporation",
"connectionType": "cable"
}

Click here to see this script in action.

As a side-note, remember that under GDPR, IP addresses are considered personal data. Tracking the IPs of your EEA based users without their consent falls under the rules of GDPR.

Find a Data Extension and it’s folder path using SSJS

If you work with a lot of Data Extensions in multiple folders, you know the pain of using the built-in search feature. It only searches the folder that you’re in, with the exclusion of any sub-folders, which is simply impractical.

Below, I would like to show you how to find a Data Extension and it’s path using Server-Side JavaScript: which functions to use and how to iterate through them.

If you’re not interested in the technical part and want to jump straight into building the search app, click here.

SSJS Data Extension and Folder Functions

In order to find a Data Extension, we will use two SSJS Core functions: DataExtension.Retrieve and Folder.Retrieve. The first function will help us identify the Data Extension, either by its name or the External Key. The second function retrieves an array of folders based on the specified criteria. It also retrieves details regarding the parent folder of a given folder if one exists, and that’s what we will use to build the Data Extension path.

<script runat="server">
Platform.Load("core","1.1.5");
var DEprop = //use either "Name" or "Customer Key"
var DEval = //provide either the Name or External Key
var FindDE = DataExtension.Retrieve({Property:DEprop,SimpleOperator:"equals",Value:DEval});
var FolderID = FindDE[0].CategoryID;
var DEname = FindDE[0].Name;
var list = [];
list.push(DEname);
var path = function(id) {
if (id> 0) {
var results = Folder.Retrieve({Property:"ID",SimpleOperator:"equals",Value:id});
list.unshift(results [0].Name);
return path(results[0].ParentFolder.ID);
} else {
return id;
}
};
path(FolderID);
Write(list.join("> "));
</script>
view raw ssjs-find-de-path.js hosted with ❤ by GitHub

In the above script, you will need to define two variables, DEprop and DEval, depending on whether you have the name or the External Key of the Data Extension. You can paste the script on a CloudPage, define the two variables, and once you click on the “Publish” button, the script will be executed and results will be visible on-screen, without the need to actually publish the CloudPage:

Create a simple Data Extension search app

If users of your instance of Salesforce Marketing Cloud often struggle with finding Data Extensions, you can create an app on a CloudPage to help them navigate the folders. By adding a simple form, you can enable the users to perform a search from a CloudPage:

Paste the below code onto a CloudPage and publish it. Anyone with access to the link will be able to search for Data Extensions located in the Business Unit where the CloudPage was created. If the CloudPage has been created in the Parent Business Unit, they will also be able to search the Shared Data Extensions folder.

<table style="padding: 20px;"><tr><td>
<b>How would you like to identify the Data Extension?</b><br>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<select name="DEprop">
<option value="Name">Name</option>
<option value="CustomerKey">External Key</option>
</select>
equals
<input type="text" name="DEval" value="" maxlength="128"><br>
<input type="submit" value="Submit">
</form><br><b>Folder path: </b>
<script runat="server">
Platform.Load("core","1.1.5");
var DEprop = Request.GetQueryStringParameter("DEprop");
var DEval = Request.GetQueryStringParameter("DEval");
var FindDE = DataExtension.Retrieve({Property:DEprop,SimpleOperator:"equals",Value:DEval});
var FolderID = FindDE[0].CategoryID;
var DEname = FindDE[0].Name;
var list = [];
list.push(DEname);
var path = function(id) {
if (id> 0) {
var results = Folder.Retrieve({Property:"ID",SimpleOperator:"equals",Value:id});
list.unshift(results [0].Name);
return path(results[0].ParentFolder.ID);
} else {
return id;
}
};
path(FolderID);
Write(list.join("> "));
</script>
</td></tr></table>

Last but not least, always remember to put security measures into practice when setting up this kind of functionality on a CloudPage, to prevent your data from being exposed in a breach.

PS. There is a free tool available called SFMC Object Finder. It’s a Chrome extension that lets you search for a variety of objects, like images, data extensions and other assets, in your instance of Salesforce Marketing Cloud. All it requires is that you have an active session of Salesforce Marketing Cloud open in your Chrome browser. However, if you cannot install this extension or need to be able to find Data Extensions programmatically, you can use Server-Side JavaScript to achieve it!

Unsubscribe and Log an UnsubEvent with a LogUnsubEvent Execute Call

The LogUnsubEvent provides a convenient way of handling unsubscribes when you create your own unsubscribe landing page or profile center functionality. This call allows you to unsubscribe a subscriber and log an UnsubEvent that is tracked against a specific Job, which means that you will be able to track from which email they unsubscribed and see the results on the tracking dashboard. You can configure the call to either unsubscribe a subscriber from a specific list, publication list, or all subscribers, which will effectively unsubscribe them from receiving any emails.

What’s more important, if you’re using Marketing Cloud Connect to connect with your Salesforce Sales or Service Cloud org, the unsubscribe will be captured on the subscriber record in Sales/Service Cloud. It will automatically check the Email Opt Out (HasOptedOutOfEmail) flag and will also be visible in the Individual Email Results for the corresponding email send.

The LogUnsubEvent Execute call uses the following parameters:

  • SubscriberID – The Marketing Cloud generated ID that uniquely identifies a subscriber.
  • SubscriberKey – The client supplied ID that uniquely identifies a subscriber.
  • EmailAddress – The email address of the subscriber.
  • JobID – The ID of the Job that sent the message.
  • ListID – The ID of the List that the subscriber belonged to. You can use subscriber or publication lists (not suppression lists).
  • BatchID – The ID of the Batch within the Job.
  • Reason – (Optional) The reason the subscriber is being unsubscribed.

The parameters can be divided into 3 sections:

  1. Subscriber context
  2. Job context
  3. Unsub reason

If you make this call from the parent unit of an Enterprise 2.0 account, ensure that you include the ClientID of the child business account to return information specific to that business unit.

Subscriber Context

The Subscriber Context is defined by the SubscriberID, SubscriberKey and EmailAddress parameters. You must supply at least one of these parameters. If you provide more than one of these parameters, we retrieve the Subscriber using one of the values and validate that the other values match the retrieved Subscriber. If they don’t match, an error returns.

If the SubscriberKey permission is turned on and you supply the EmailAddress parameter, you must supply either the SubscriberID or the SubscriberKey.

Job Context

The Job Context is defined by the JobID, ListID and BatchID parameters. These values are used to determine which Job the UnsubEvent is tracked against. The subscriber is also unsubscribed from the List that the Job was sent to. You don’t need to supply all three values. The system looks up any missing values using the following rules:

  1. If the JobID is supplied, we can lookup a missing ListID and/or BatchID.
  2. If the ListID is supplied, we can lookup a missing JobID and/or BatchID.
    1. If the JobID is missing, we use the most recent JobID that the subscriber was sent to.
    2. This may not be the Job that the Subscriber is acting upon.
  3. If only the BatchID is supplied, we cannot lookup the remaining information and the job context is not defined.

If the job context cannot be established because you did not supply any of these parameters or only supplied the BatchID, the UnsubEvent is not created. The subscriber is also Master Unsubscribed from the system of being unsubscribed from a particular list. Remove the ListID to address the All Subscribers list in an account.

Unsub Reason

This is used to specify the reason the subscriber is being unsubscribed from the system. If the reason is not supplied, the default value is used: Unsubscribed via Log Unsub Event Execute call.

Here is an example SOAP Request envelope:

<soap-ENV:Body>
<ExecuteRequestMsg xmlns="http://exacttarget.com/wsdl/partnerAPI">
<Requests>
<Name>LogUnsubEvent</Name>
<Parameters>
<Name>SubscriberID</Name>
<Value>123456</Value>
</Parameters>
<Parameters>
<Name>SubscriberKey</Name>
<Value>Key for username@example.com</Value>
</Parameters>
<Parameters>
<Name>EmailAddress</Name>
<Value>help@example.com</Value>
</Parameters>
<Parameters>
<Name>JobID</Name>
<Value>18099</Value>
</Parameters>
<Parameters>
<Name>ListID</Name>
<Value>17914</Value>
</Parameters>
<Parameters>
<Name>BatchID</Name>
<Value>0</Value>
</Parameters>
</Requests>
</ExecuteRequestMsg>
</SOAP-ENV:Body>

We will now look at three different ways to implement this solution on a CloudPage.

LogUnsubEvent using AMPscript

This is probably the most common way to use the LogUnsubEvent call. Below script will retrieve the SubscriberKey, JobId, ListId and BatchId if you link to the unsubscribe page from your email using the CloudPagesURL function. Depending on whether a list/publication list was selected for the send, it will unsubscribe a subscriber from that particular list, or it will unsubscribe the subscriber from all emails if you used All Subscribers.

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[
VAR @skey, @jid, @reason, @lue, @lue_prop, @lue_statusCode, @overallStatus, @requestId, @Response, @Status, @Error
SET @skey = _subscriberkey
SET @jid = RequestParameter("jobid")
SET @listid = RequestParameter("listid")
SET @batchid = RequestParameter("batchid")
SET @reason = "AMPscript one click unsubscribe"
SET @lue = CreateObject("ExecuteRequest")
SetObjectProperty(@lue,"Name","LogUnsubEvent")
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "SubscriberKey")
SetObjectProperty(@lue_prop, "Value", @skey)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "JobID")
SetObjectProperty(@lue_prop, "Value", @jid)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "ListID")
SetObjectProperty(@lue_prop, "Value", @listid)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "BatchID")
SetObjectProperty(@lue_prop, "Value", @batchid)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_prop = CreateObject("APIProperty")
SetObjectProperty(@lue_prop, "Name", "Reason")
SetObjectProperty(@lue_prop, "Value", @reason)
AddObjectArrayItem(@lue, "Parameters", @lue_prop)
SET @lue_statusCode = InvokeExecute(@lue, @overallStatus, @requestId)
SET @Response = Row(@lue_statusCode, 1)
SET @Status = Field(@Response,"StatusMessage")
SET @Error = Field(@Response,"ErrorCode")
]%%
<script runat="server">
}catch(e){
Write(Stringify(e));
}
</script>
@Response: %%=v(@Response)=%%<br>
@Status: %%=v(@Status)=%%<br>
@Error: %%=v(@Error)=%%<br>

LogUnsubEvent using Server-Side JavaScript

The implementation of the LogUnsubEvent call in SSJS will be almost identical to the AMPscript solution, as the methods for accessing SOAP object data with SSJS are primarily wrappers around AMPScript functions.

Like in the previous example, the script will retrieve the SubscriberKey, JobId, ListId and BatchId if you link to the unsubscribe page from your email using the CloudPagesURL function and will unsubscribe a subscriber either from a list or All Subscribers, depending on which one you use at send time.

<script runat="server">
Platform.Load("core","1");
var subkey, jid, lue, lid, bid, lue_prop, Response;
var subkey = Attribute.GetValue("_subscriberkey");
var jid = Attribute.GetValue("jobid");
var lid = Attribute.GetValue("listid");
var bid = Attribute.GetValue("_JobSubscriberBatchID");
lue = Platform.Function.CreateObject("ExecuteRequest");
Platform.Function.SetObjectProperty(lue,"Name","LogUnsubEvent");
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "SubscriberKey");
Platform.Function.SetObjectProperty(lue_prop, "Value", subkey);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "JobID");
Platform.Function.SetObjectProperty(lue_prop, "Value", jid);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "ListID");
Platform.Function.SetObjectProperty(lue_prop, "Value", lid);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "BatchID");
Platform.Function.SetObjectProperty(lue_prop, "Value", bid);
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
lue_prop = Platform.Function.CreateObject("APIProperty");
Platform.Function.SetObjectProperty(lue_prop, "Name", "Reason");
Platform.Function.SetObjectProperty(lue_prop, "Value", "SSJS one click unsubscribe");
Platform.Function.AddObjectArrayItem(lue, "Parameters", lue_prop);
var statusAndRequest = [0,0];
try{
Response = Platform.Function.InvokeExecute(lue, statusAndRequest);
Write(Stringify(Response))
}catch(e){
Write(Stringify(e));
}
</script>

LogUnsubEvent using WSProxy

WSProxy is a new object for Server-Side JavaScript, introduced by Salesforce in 2018. It acts as a proxy between the Marketing Cloud SOAP Web Service and SSJS. The WSProxy object is native to the platform and simpler to use than the SSJS methods. The object reduces overhead and increases the speed of your API calls.

Therefore, the below script is the most simple, and what’s more important, the fastest way to execute the LogUnsubEvent API call. In a speed test ran using the Chrome DevTools, it proved to be the fastest one to load on a CloudPage, with AMPscript just slightly slower and SSJS the slowest one, taking twice as much time to load.

Just like in the previous examples, the script will retrieve the SubscriberKey, JobId, ListId and BatchId if you link to the unsubscribe page from your email using the CloudPagesURL function and will unsubscribe a subscriber either from a list or All Subscribers, depending on which one you use at send time.

<script runat="server">
Platform.Load("Core","1.1.1");
var subkey = Attribute.GetValue("_subscriberkey");
var jid = Attribute.GetValue("jobid");
var lid = Attribute.GetValue("listid");
var bid = Attribute.GetValue("_JobSubscriberBatchID");
var prox = new Script.Util.WSProxy();
var props = [
{ Name: "SubscriberKey", Value: subkey },
{ Name: "JobID", Value: jid },
{ Name: "ListID", Value: lid },
{ Name: "BatchID", Value: bid },
{ Name: "Reason", Value: "WSProxy one click unsubscribe" }
];
try{
var data = prox.execute(props, "LogUnsubEvent");
Write(Stringify(data));
}catch(e){
Write(Stringify(e));
}
</script>

If you would like to find out more about logging an UnsubEvent or WSProxy, check out the following articles: