Consolidate tracking data using SQL

There are many use cases for consolidating and transforming email tracking data in Salesforce Marketing Cloud, and they are usually related to a requirement for sending the tracking data in a given format to external systems, such as databases and data warehouses. We will use Salesforce Marketing Cloud’s Data Views to get all the details of the email job, opens, clicks, bounces and unsubscribes, as well as subscriber details.

Create a target Data Extension

Before we start with the query, let’s first create a target Data Extension to hold the results. The Data Extension should have the following columns and it shouldn’t have any primary keys. It also doesn’t need to be sendable or testable.

NameData TypeLengthNullable
SubscriberKeyText100no
EmailAddressEmail255no
EmailNameText100yes
EmailSubjectText200yes
FromNameText130yes
FromEmailText100yes
SentDateDateyes
OpenDateDateyes
NumberOfOpensNumberyes
ClickDateDateyes
NumberOfClicksNumberyes
NumberOfLinksClickedNumberyes
BouncedDateDateyes
BounceCategoryText50yes
UnsubscribedDateDateyes

Joining data from Data Views

When working with data from Data Views in SQL, it’s important to correctly join the data from the different tables. Even though the diagram below shows that common columns that can be used for joining data are SubscriberID and JobID, you should in fact join on more than those two columns to get correct results from your queries.

This is especially important for Journey Builder sends, where every email that is sent out individually will have a different BatchID value assigned, so in case the same person enters the same journey multiple times and gets the same email more than once, you could potentially end up with incorrect tracking data if you only join on SubscriberID and JobID.

The most reliable way to join tracking data from Data Views is to actually use all the common column names that are present in the _Sent, _Open, _Click, _Bounce and _Unsubscribe tables, just like in the example below, where we are joining data on JobID, ListID, BatchID and SubscriberID:

select
s.subscriberkey
from _sent s
left join _open o
on s.jobid = o.jobid and s.listid = o.listid and s.batchid = o.batchid and s.subscriberid = o.subscriberid and o.isunique = 1
left join _click c
on s.jobid = c.jobid and s.listid = c.listid and s.batchid = c.batchid and s.subscriberid = c.subscriberid and c.isunique = 1

Getting the total number of opens and clicks

In our query, we will add a couple of subqueries which will allow us to get the total number of opens, clicks and URLs clicked per subscriber per email send. Below are the details of the three subqueries.

Total number of opens:

select
jobid,
ListID,
BatchID,
subscriberid,
count(subscriberid) AS Opens
from _open
group by subscriberid, jobid, ListID, BatchID

Total number of clicks:

select
jobid,
ListID,
BatchID,
subscriberid,
count(subscriberid) AS Clicks
from _click
group by subscriberid, jobid, ListID, BatchID

Total number of URLs clicked:

select distinct
jobid,
ListID,
BatchID,
subscriberid,
count(distinct url) AS URLs
from _click
group by subscriberid, jobid, ListID, BatchID

SQL to consolidate tracking data

For our final query, we will combine the data from all Data Views mentioned above with the three subqueries and we will add one more additional join with the _ListSubscribers Data View to be able to display the email addresses of subscribers. If you’re running this query in a Child BU, make sure to use ent._ListSubscribers instead of _ListSubscribers.

SELECT
s.SubscriberKey,
ls.EmailAddress,
j.EmailName,
j.EmailSubject,
j.FromName,
j.FromEmail,
s.EventDate AS SentDate,
o.EventDate AS OpenDate,
op.Opens AS NumberOfOpens,
c.EventDate AS ClickDate,
cl.Clicks AS NumberOfClicks,
url.URLs AS NumberOfLinksClicked,
b.EventDate AS BouncedDate,
b.BounceCategory,
u.EventDate AS UnsubscribedDate
FROM _Sent s
LEFT JOIN _Job j ON s.JobID = j.JobID
LEFT JOIN _Open o ON s.JobID = o.JobID
AND s.ListID = o.ListID
AND s.BatchID = o.BatchID
AND s.SubscriberID = o.SubscriberID
AND o.IsUnique = 1
LEFT JOIN _Click c ON s.JobID = c.JobID
AND s.ListID = c.ListID
AND s.BatchID = c.BatchID
AND s.SubscriberID = c.SubscriberID
AND c.IsUnique = 1
LEFT JOIN _Bounce b ON s.JobID = b.JobID
AND s.ListID = b.ListID
AND s.BatchID = b.BatchID
AND s.SubscriberID = b.SubscriberID
AND b.IsUnique = 1
LEFT JOIN _Unsubscribe u ON s.JobID = u.JobID
AND s.ListID = u.ListID
AND s.BatchID = u.BatchID
AND s.SubscriberID = u.SubscriberID
AND u.IsUnique = 1
LEFT JOIN (select
jobid,
ListID,
BatchID,
subscriberid,
count(subscriberid) AS Clicks
from _click
group by subscriberid, jobid, ListID, BatchID) cl ON s.JobID = cl.JobID
AND s.ListID = cl.ListID
AND s.BatchID = cl.BatchID
AND s.SubscriberID = cl.SubscriberID
LEFT JOIN (select distinct
jobid,
ListID,
BatchID,
subscriberid,
count(distinct url) AS URLs
from _click
group by subscriberid, jobid, ListID, BatchID) url ON s.JobID = url.JobID
AND s.ListID = url.ListID
AND s.BatchID = url.BatchID
AND s.SubscriberID = url.SubscriberID
LEFT JOIN (select
jobid,
ListID,
BatchID,
subscriberid,
count(subscriberid) AS Opens
from _open
group by subscriberid, jobid, ListID, BatchID) op ON s.JobID = op.JobID
AND s.ListID = op.ListID
AND s.BatchID = op.BatchID
AND s.SubscriberID = op.SubscriberID
LEFT JOIN _ListSubscribers ls ON s.SubscriberID = ls.SubscriberID

You can now automate this query in Automation Studio and create a recurring export of the results and send them to the FTP for consumption by external systems.


Questions? Comments?

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

Updated Data Views visualization

It’s been one and a half years since the original article about Data Views has been published and since then, it’s become the most viewed article on sfmarketing.cloud. I’ve received many questions and comments about it over time, and have decided it’s high time to update the Data Views visualization.

Here’s what has been revised:

  • Added the new BusinessUnitUnsubscribes Data View, which allows you to find subscribers in your account and their child business unit unsubscribe data
  • Added the GroupConnect Data Views, GroupConnect and MobileLineOrphanContactView which allow you to to view active LINE followers, users who have blocked your brand and orphaned contacts for the same LINE Address ID in GroupConnect Contact Import
  • Added relationships between MobileConnect Data Views, SMSMessagetracking, SMSSubscriptionLog and UndeliverableSMS
  • Added commas after column names for easier copy & paste into Query Studio

The new visualization is available below in PNG format (click on the photo to enlarge it) and in PDF format that you can download here.

Please let me know in case you have any comments or further questions about the Data Views or the visualization, I will be happy to address them during the next revision.


Questions? Comments?

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

Add multiple contacts to a Journey in one batch using REST API

Firing an Entry Event is great for injecting single contacts into a Salesforce Marketing Cloud journey from external systems or websites – but what if you need to inject contacts to a journey in bulk?

After reading a recent question on Salesforce Stack exchange, I found myself playing with the unofficial REST endpoint /interaction-experimental/v1/batchcontactevents, which is used by the Marketing Cloud Connector managed package in Salesforce to inject contacts in bulk into journeys, for example, if you’re adding a batch of contacts into a Salesforce Campaign used as a journey entry source.

If you would like to see for yourself how this works, you can initiate a Marketing Cloud Connector log from the Marketing Cloud tab in Salesforce and add a few contacts to a running Campaign. The log will register the details of your call along with the REST endpoint and the JSON payload:

*****2020-11-12 16:51:04.337|HTTP RESPONSE|"6ffc2d53-c97e-4a89-bd66-535cxxxxx"
*****2020-11-12 17:02:29.36|HTTP CALLOUT|FireJourneyBuilderEvent
*****2020-11-12 17:02:29.46|HTTP REQUEST|https://mcxxxxx.rest.marketingcloudapis.com/interaction-experimental/v1/batchcontactevents
{
"EventDefinitionKey":"SalesforceObjcbd44d55f8f9df29b0f5631xxxxxx",
"ContactPersonType":"CampaignMember",
"CallerSystemName":"Salesforce",
"BatchId":"",
"RecordCount":2,
"CalloutTimeStamp":"12/11/2020 17:02",
"BatchDetails":[
{
"ContactKey":"0031t00000XXX8UAAS",
"Data":{
"CampaignMember:Id":"00v1t00000XXXOEAA0",
"CampaignMember:Common:Id":"0031t00000XXX8UAAS",
"CampaignMember:Common:Email":"zuzanna@sfmarketing.cloud",
"CampaignMember:Common:HasOptedOutOfEmail":false,
"MemberRecordType":"Contact"
}
},
{
"ContactKey":"0031t00000XXXWMAA1",
"Data":{
"CampaignMember:Id":"00v1t00000XXXOFAA0",
"CampaignMember:Common:Id":"0031t00000XXXWMAA1",
"CampaignMember:Common:Email":"info@ampscript.io",
"CampaignMember:Common:HasOptedOutOfEmail":true,
"MemberRecordType":"Contact"
}
}
]
}
*****2020-11-12 17:02:29.52|HTTP RESPONSE|"8dfb0fa9-40e2-432c-adfa-a742c6xxxxx"

Based on above, I have been able to build a payload to use with other journeys that have an API entry event.

DISCLAIMER: As this is an experimantal, undocumented endpoint, you need to use it with caution and on your own responsibility. It’s not supported and could be disabled without a warning.

Authentication

The major downside of using this endpoint that I discovered, is that it requires you to use the “old” v1 access token (24 digit). This means that if you’re on a newer Salesforce Marketing Cloud instance where you haven’t been able to install the legacy package before they were discontinued, you won’t be able to use the /interaction-experimental/v1/batchcontactevents endpoint. If you are on an older instance where you still have an access to a legacy package, follow requestToken Reference for Legacy Packages to obtain an OAuth token.

JSON Parameters

To build your request, make sure you already have a journey with an API Entry Event and the list of associated Data Extension fields. In the JSON payload, you will need to include the API EventDefinitionKey, the number of contacts you are sending and their details: ContactKey is required for every contact included in your payload and any additional Data Extension fields need to be included in the Data object. Below I have included two contacts and two Data Extension fields, email and firstName:

Host: https://YOUR_SUBDOMAIN.rest.marketingcloudapis.com
POST /interaction-experimental/v1/batchcontactevents
Content-Type: application/json
Authorization: Bearer YOUR_V1_ACCESS_TOKEN
{
"EventDefinitionKey":"{{API entry event key}}",
"BatchId":"",
"RecordCount":2,
"BatchDetails":[
{
"ContactKey":"{{Contact key}}",
"Data":{
"email":"{{email address}}",
"firstName":"{{first name}}"
}
},
{
"ContactKey":"{{Contact key}}",
"Data":{
"email":"{{email address}}",
"firstName":"{{first name}}"
}
}
]
}

According to this Salesforce Stack Exchange post, above works up to 500 records per batch, which means that you will need to split bigger payloads into batches.

During testing, I noticed that the response is always 201 “Created”, even if the contacts did not go through, so error handling might be tricky. The response itself returns an eventInstanceId.

If you have also been playing with this endpoint and feel that something is worth adding to the above, feel free to reach out!


Questions? Comments?

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

Working with Salesforce Multi-Select Picklists in AMPscript

Custom Multi-Select Picklists in Sales/Service Cloud allow users to select one or more values from a predefined list:

When a user picks more than one value, the selected values show in a field separated by a semicolons:

The values will be in the same, semicolon-delimited format, if you synchronize the picklist field into Marketing Cloud and store the data in a Data Extension. This means that you will have to break each of those fields up into separate values to be able to use them in your script.

My example multi-select picklist in Sales Cloud is used for storing subscriber preferences and has five possible values:

  • Events
  • Newsletters
  • Promotions
  • Special Offers
  • Surveys

In this tutorial, we will first pull the multi-select picklist data onto a CloudPage to display it for the subscriber as an HTML form, where they can pick which of the above they are interested in, and once they submit the form, we will write that data back into Sales Cloud.

Splitting a string by a delimiter

First, let’s pull the data into our script. In the example below, I will use the RetrieveSalesforceObjects() function to pull the data onto a CloudPage directly from Sales/Service Cloud. Depending on your use case, you might want to use the Lookup() or LookupRows() functions instead, if you are working with Synchronized Data Extensions.

My multi-select picklist field is called Preferences__c and it’s on the Contact object:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberrows = retrievesalesforceobjects(
"contact",
"firstname,lastname,email,preferences__c",
"id", "=", @subscriberKey)
if rowcount(@subscriberrows) == 1 then
set @subscriberrow = row(@subscriberrows, 1)
set @firstname = field(@subscriberrow, "firstname")
set @lastname = field(@subscriberrow, "lastname")
set @email = field(@subscriberrow, "email")
set @preferences__c = field(@subscriberrow, "preferences__c")
endif
]%%
%%=v(@preferences__c)=%%

When you output the data using %%=v(@preferences__c)=%%, you will see it in exactly the same semicolon-delimited format as mentioned earlier:

In order to split multi-select picklist data into separate values and to be able to assign those values to AMPscript variables, we will use the BuildRowSetFromString() function. The BuildRowSetFromString function will create a rowset from a character string by splitting the string at the specified delimiter – in our case, a semicolon.

As a first step, we will check if our @preferences__c variable has any data inside by using the Empty() function. If the variable is not empty, we can proceed with the BuildRowSetFromString function:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberrows = retrievesalesforceobjects(
"contact",
"firstname,lastname,email,preferences__c",
"id", "=", @subscriberKey)
if rowcount(@subscriberrows) == 1 then
set @subscriberrow = row(@subscriberrows, 1)
set @firstname = field(@subscriberrow, "firstname")
set @lastname = field(@subscriberrow, "lastname")
set @email = field(@subscriberrow, "email")
set @preferences__c = field(@subscriberrow, "preferences__c")
endif
if not empty(@Preferences__c) then
set @rs = buildrowsetfromstring(@preferences__c,';')
Output(rowcount(@rs))
endif
]%%

Output(rowcount(@rs)) will output the number of values contained in our multi-select picklist field.

Now let’s add a loop to iterate through the rowset and match the values in our script with multi-select picklist values from Saleforce. Remember, that the more picklist values you have in Salesforce, the more complicated your loop will get.

The biggest challenge we need to tackle in our script, is the fact that we never know how many and which values each subscriber will have assigned to them. That’s why we need to iterate through all of the values and use a conditional statement to check if each of the values found matches a picklist value we know exists in Salesforce.

Here’s how the script will look like once we add our loop with the conditional statements:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberRows = RetrieveSalesforceObjects(
"Contact",
"FirstName,LastName,Email,Preferences__c",
"Id", "=", @subscriberKey )
if RowCount(@subscriberRows) == 1 then /* there should only be one row */
var @subscriberRow, @firstName, @lastName, @email
set @subscriberRow = Row(@subscriberRows, 1)
set @firstName = Field(@subscriberRow, "FirstName")
set @lastName = Field(@subscriberRow, "LastName")
set @email = Field(@subscriberRow, "Email")
set @Preferences__c = Field(@subscriberRow, "Preferences__c")
IF NOT EMPTY(@Preferences__c) THEN
SET @rs = BuildRowsetFromString(@Preferences__c,';')
IF rowcount(@rs) > 0 THEN
FOR @i=1 TO rowcount(@rs) DO
SET @val = Field(Row(@rs,@i),1)
IF @val == "Surveys" THEN
SET @Surveys = true
ELSEIF @val == "Events" THEN
SET @Events = true
ELSEIF @val == "Newsletters" THEN
SET @Newsletters = true
ELSEIF @val == "Promotions" THEN
SET @Promotions = true
ELSEIF @val == "Special Offers" THEN
SET @Offers = true
ENDIF
NEXT @i
ELSE
SET @Newsletters = ""
SET @Events = ""
SET @Offers = ""
SET @Survays = ""
SET @Promotions = ""
ENDIF
ENDIF
endif
]%%
@Surveys: %%=v(@Surveys)=%%<br>
@Events: %%=v(@Events)=%%<br>
@Newsletters: %%=v(@Newsletters)=%%<br>
@Promotionss: %%=v(@Promotions)=%%<br>
@Offers: %%=v(@Offers)=%%<br>

Above script will display true for all values where it found a match.

Let’s now add an HTML form and display our picklist data as checkboxes.

HTML checkboxes and AMPscript

We will use AMPscript and the values retrieved using the BuildRowSetFromString function to conditionally mark checkboxes as checked if there is a matching value in Salesforce. In HTML forms, the checked attribute specifies that an element should be pre-selected when the page loads (for type=”checkbox” or type=”radio”):

<input type="checkbox" checked>

For each of the checkboxes representing the multi-select picklist values, we will add an AMPscript variable to conditionally add the checked attribute when the value is found in our rowset. Let’s also add the basic subscriber information like First Name, Last Name and Email to the form:

<h2>Please fill in the form:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<label>First name: </label><input type="text" name="firstname" required="" value="%%=v(@firstName)=%%"><br>
<label>Last name: </label><input type="text" name="lastname" required="" value="%%=v(@lastName)=%%"><br>
<label>Email: </label><input type="text" name="email" required="" value="%%=v(@email)=%%"><br><br>
<h2>Update your subscriptions:</h2>
<input name="newsletters" type="checkbox" %%=v(@Newsletters)=%%><label>Newsletters</label><br>
<input name="events" type="checkbox" %%=v(@Events)=%%><label>Events</label><br>
<input name="offers" type="checkbox" %%=v(@Offers)=%%><label>Special Offers</label><br>
<input name="surveys" type="checkbox" %%=v(@Surveys)=%%><label>Surveys</label><br>
<input name="promotions" type="checkbox" %%=v(@Promotions)=%%><label>Promotions</label><br>
<input name="submitted" type="hidden" value="true"><br>
<input type="submit" value="Submit">
</form>

Now we need to adjust or script to display the word checked instead of true in case there is a match:

%%[
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberRows = RetrieveSalesforceObjects(
"Contact",
"FirstName,LastName,Email,Preferences__c",
"Id", "=", @subscriberKey )
if RowCount(@subscriberRows) == 1 then /* there should only be one row */
var @subscriberRow, @firstName, @lastName, @email
set @subscriberRow = Row(@subscriberRows, 1)
set @firstName = Field(@subscriberRow, "FirstName")
set @lastName = Field(@subscriberRow, "LastName")
set @email = Field(@subscriberRow, "Email")
set @Preferences__c = Field(@subscriberRow, "Preferences__c")
IF NOT EMPTY(@Preferences__c) THEN
SET @rs = BuildRowsetFromString(@Preferences__c,';')
IF rowcount(@rs) > 0 THEN
FOR @i=1 TO rowcount(@rs) DO
SET @val = Field(Row(@rs,@i),1)
IF @val == "Surveys" THEN
SET @Surveys = "checked"
ELSEIF @val == "Events" THEN
SET @Events = "checked"
ELSEIF @val == "Newsletters" THEN
SET @Newsletters = "checked"
ELSEIF @val == "Promotions" THEN
SET @Promotions = "checked"
ELSEIF @val == "Special Offers" THEN
SET @Offers = "checked"
ENDIF
NEXT @i
ELSE
SET @Newsletters = ""
SET @Events = ""
SET @Offers = ""
SET @Survays = ""
SET @Promotions = ""
ENDIF
ENDIF
endif
]%%
<h2>Please fill in the form:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<label>First name: </label><input type="text" name="firstname" required="" value="%%=v(@firstName)=%%"><br>
<label>Last name: </label><input type="text" name="lastname" required="" value="%%=v(@lastName)=%%"><br>
<label>Email: </label><input type="text" name="email" required="" value="%%=v(@email)=%%"><br><br>
<h2>Update your subscriptions:</h2>
<input name="newsletters" type="checkbox" %%=v(@Newsletters)=%%><label>Newsletters</label><br>
<input name="events" type="checkbox" %%=v(@Events)=%%><label>Events</label><br>
<input name="offers" type="checkbox" %%=v(@Offers)=%%><label>Special Offers</label><br>
<input name="surveys" type="checkbox" %%=v(@Surveys)=%%><label>Surveys</label><br>
<input name="promotions" type="checkbox" %%=v(@Promotions)=%%><label>Promotions</label><br>
<input name="submitted" type="hidden" value="true"><br>
<input type="submit" value="Submit">
</form>

Now you should be able to correctly display the multi-select picklist data for each subscriber visiting your CloudPage.

Update Multi-Select Picklist values in Salesforce using AMPscript

Once the subscriber makes changes to their preferences and clicks on the Submit button, the page will reload and the form parameters will be posted back to the same page. If you would like to learn more about this approach of working with forms on CloudPages, take a look at one of my earlier articles: Create a Sales Cloud-integrated lead capture form using AMPscript.

Now we can add the script to process the posted form data and update it in Salesforce:

SET @preferences = CONCAT(
Iif(RequestParameter("newsletters") == "on", "Newsletters;", ""),
Iif(RequestParameter("events") == "on", "Events;", ""),
Iif(RequestParameter("offers") == "on", "Special Offers;", ""),
Iif(RequestParameter("surveys") == "on", "Surveys;", ""),
Iif(RequestParameter("promotions") == "on", "Promotions", ""),
)
if not Empty(@preferences) then
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"Preferences__c", @preferences)
else
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"fieldsToNull", "Preferences__c")
endif

To pass the data back to Salesforce, we need to transform the posted form data back into the initial form: a string with semicolon-delimited values. In order to achieve this, I have used the Iif() function to conditionally check which values have been selected in the form (a selected checkbox will return the value of on). If a parameter returns the on value, we substitute it with the corresponding picklist value and concatenate all the returned values together into one string to pass them to Salesforce.

The full script

Here is the full script, including a try/catch statement for debugging:

<script runat="server">
Platform.Load("Core","1.1.1");
try{
</script>
%%[
IF RequestParameter("submitted") == true THEN
SET @preferences = CONCAT(
Iif(RequestParameter("newsletters") == "on", "Newsletters;", ""),
Iif(RequestParameter("events") == "on", "Events;", ""),
Iif(RequestParameter("offers") == "on", "Special Offers;", ""),
Iif(RequestParameter("surveys") == "on", "Surveys;", ""),
Iif(RequestParameter("promotions") == "on", "Promotions", ""),
)
if not Empty(@preferences) then
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"Preferences__c", @preferences)
else
SET @updateRecord = UpdateSingleSalesforceObject(
"Contact", RequestParameter("subkey"),
"FirstName", RequestParameter("firstname"),
"LastName", RequestParameter("lastname"),
"Email", RequestParameter("email"),
"fieldsToNull", "Preferences__c")
endif
]%%
Your preferences have been updated.
%%[ELSE
set @subscriberKey = AttributeValue(_subscriberKey)
set @subscriberRows = RetrieveSalesforceObjects(
"Contact",
"FirstName,LastName,Email,Preferences__c",
"Id", "=", @subscriberKey)
if RowCount(@subscriberRows) == 1 then
var @subscriberRow, @firstName, @lastName, @email
set @subscriberRow = Row(@subscriberRows, 1)
set @firstName = Field(@subscriberRow, "FirstName")
set @lastName = Field(@subscriberRow, "LastName")
set @email = Field(@subscriberRow, "Email")
set @Preferences__c = Field(@subscriberRow, "Preferences__c")
IF NOT EMPTY(@Preferences__c) THEN
SET @rs = BuildRowsetFromString(@Preferences__c,';')
IF rowcount(@rs) > 0 THEN
FOR @i=1 TO rowcount(@rs) DO
SET @val = Field(Row(@rs,@i),1)
IF @val == "Surveys" THEN
SET @Surveys = "checked"
ELSEIF @val == "Events" THEN
SET @Events = "checked"
ELSEIF @val == "Newsletters" THEN
SET @Newsletters = "checked"
ELSEIF @val == "Promotions" THEN
SET @Promotions = "checked"
ELSEIF @val == "Special Offers" THEN
SET @Offers = "checked"
ENDIF
NEXT @i
ELSE
SET @Newsletters = ""
SET @Events = ""
SET @Offers = ""
SET @Survays = ""
SET @Promotions = ""
ENDIF
ENDIF
endif
]%%
<h2>Please fill in the form:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<label>First name: </label><input type="text" name="firstname" required="" value="%%=v(@firstName)=%%"><br>
<label>Last name: </label><input type="text" name="lastname" required="" value="%%=v(@lastName)=%%"><br>
<label>Email: </label><input type="text" name="email" required="" value="%%=v(@email)=%%"><br><br>
Pref: %%=v(@Preferences__c)=%%<br><br>
<h2>Update your subscriptions:</h2>
<input name="newsletters" type="checkbox" %%=v(@Newsletters)=%%><label>Newsletters</label><br>
<input name="events" type="checkbox" %%=v(@Events)=%%><label>Events</label><br>
<input name="offers" type="checkbox" %%=v(@Offers)=%%><label>Special Offers</label><br>
<input name="surveys" type="checkbox" %%=v(@Surveys)=%%><label>Surveys</label><br>
<input name="promotions" type="checkbox" %%=v(@Promotions)=%%><label>Promotions</label><br>
<input name="subkey" type="hidden" value="%%=v(@subscriberKey)=%%"><br>
<input name="submitted" type="hidden" value="true"><br>
<input type="submit" value="Submit">
</form>
%%[ ENDIF ]%%
<script runat="server">
}catch(e){
Write(Stringify(e));
}
</script>

To see this script in action, visit my CloudPage here.


Questions? Comments?

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

Troubleshooting Marketing Cloud Connect Business Unit mapping

This quick tutorial will come in hand for anyone who is making changes to their Marketing Cloud Connect and Business Unit setup. Whether you’re trying to remove Business Unit access for an existing Salesforce Marketing Cloud API user or you’re disconnecting your current connection and replacing it with a brand new one, it’s possible that you will come across the following error:

Sorry, something went wrong.
We weren’t able to retrieve your business units. Check to make sure you have permission to access these business units and try again.
Need more help? Turn on logging and open a support case in the Help and Training portal.

Above message indicates that the previous connection in Salesforce has not been fully removed and the connector still tries to retrieve the business units that were set up earlier on top of the new business units.

Removing the Configuration will not always help with removing Business Unit mapping, so in case you see the error message, try the following:

  1. In Salesforce Marketing Cloud, make sure that the SFMC API user has access to the correct Business Units.
  2. In Sales/Service Cloud, navigate to Setup > Tabs
  3. Under Custom Object Tabs, click New
  4. From the Object drop-down menu, choose Business Unit and pick any Tab Style that you like:

5. Click Next & Next & Save. You should now be able to see the new tab in both Classic and Lightening interfaces:

6. Click on the Business Units tab and choose to view All:

  1. Delete unwanted Business Units from the list using the Del Action.
  2. Go to the Marketing Cloud tab, click on Connect to Marketing Cloud and log in with the SFMC API user credentials.

When you click on Manage Business Units, you should now be able to choose Business Units from the correct list of Business Units that your SFMC API user has access to.


Questions? Comments?

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

Customize your Marketing Cloud account with your company’s branding

Marketing Cloud’s Brand Builder lets you easily change the color scheme of your Marketing Cloud account based on the colors of your company’s logo. The updated color scheme will be visible to both your users and your subscribers. Once you upload a file with the logo (it needs to be a *.JPG or *.GIF and maximum 550px wide by 100px high), the new color scheme will be changed for:

  • Marketing Cloud Login Page
  • Email Page
  • Marketing Cloud Application
  • Subscription Center

Source: https://help.salesforce.com/articleView?id=mc_overview_brandbuilder.htm&type=5

Create a new brand in your account

Brand Builder used to be a hidden feature, available only on request through support. Right now, after the recent changes to the Setup menu, it’s available for each account and doesn’t require any additional activation.

To start customizing your brand, go to Setup > Settings > Company Settings > BrandBuilder and click on the New Brand button. You will be asked to provide a name and the new brand will be created:

Now click on Edit to customize the brand you created. You will see a preview which uses the ExactTarget color scheme, but don’t worry about it, this will change later:

Click on Edit once more, to upload your logo and customize the name of your account. Remember to use either a *.JPG or *.GIF file and to exclude any special characters from the account name. Click on Save. After that, Marketing Cloud will analyze the colors of your logo and adjust the color scheme:

If you’re not happy with the proposed colors, you can edit them by clicking again on the Edit button and using the colors of your choice.

You can also preview each of the customized elements of your account by clicking on the Preview button:

Note that, unfortunately, some of the previews are inaccurate and still shows the old ExactTarget user interface, so do not be alarmed – the icons and menus you currently have will remain the same in your account.

Once you set all the colors, go to the Where Used tab to choose Business Units for which this brand should be applied.

Log out and log back in to see the changes. Voila!


Questions? Comments?

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

Manage Contacts and Leads in Marketing Cloud Journeys directly from your Salesforce org

This week Salesforce Labs has released a Lightning Component which, no doubt, will make the life easier for anyone working with Journey Builder and Marketing Cloud Connect.

If you have multiple Journeys set up in your account with different Salesforce Entry Events, you probably know the pain of finding out which Sales/Service Cloud Contacts and Leads are currently in the Journeys. And if you need to eject a Contact/Lead from a Journey immediately? You either end up doing it in POSTMAN or developing your own app on CloudPages just for that purpose.

Marketing Cloud Journeys for Salesforce is a Lightning Component which can be added to the Contact, Lead, Case or Account page and it will display the list of Salesforce Marketing Cloud Journeys that the subscriber is currently in, along with a stop button to allow instant removal of said subscriber from the Journey.

It’s available for free from AppExchange: Marketing Cloud Journeys for Salesforce, and you need to have Admin permissions in both Sales/Service Cloud and in Marketing Cloud to be able to install it.

Here’s the high level overview of the installation process (detailed guide can be found here):

  1. Install the managed package from AppExchange: Marketing Cloud Journeys for Salesforce
  2. Create a Package in Marketing Cloud with the component: API integration > Web App
  3. In your Salesforce org, configure Auth. Provider with type Open ID Connect and paste all the details of the Marketing Cloud API package created in the previous step
  4. In your Salesforce org, configure the Named Credential and test the Authentication Flow on save
  5. Add the Lightning Component to Record Pages by opening any Contact, Lead, Case or Account and clicking on the Setup menu at the top right and choosing Edit Page

One important prerequisite, which is not clearly described in the installation guide, is the requirement to have a “Contact Key” field on the object for which you want to display the component. You do not need to create that field separately – it’s enough that you just point to the field containing the ID used as Subscriber Key in Marketing Cloud, for example Lead Id or Contact Id:

Now the updated page will display the component and list all the Journey names and their current active version, in which the subscriber is present:

It will also give you the option to eject the subscriber from a selected Journey, or from all active Journeys:

Upon clicking on the Stop/Stop All Journeys option, you will be prompted to confirm:

Conslusion

Salesforce Labs is closing a major gap in the cross-cloud functionality with this small, yet powerful and much needed addition. The component has proven to work correctly in the few simple tests I gave it and I would recommend anyone using Salesforce Marketing Cloud’s Journey Builder with Sales/Service data to also give it a go.


Questions? Comments?

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

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.

Simple Marketing Cloud App hosted on a CloudPage

DISCLAIMER: The Marketing Cloud App component has been designed for use with externally hosted apps. Use any alternative solutions presented in this article with caution. The settings we are going to use are simplified compared to the ones required by externally hosted apps, so make sure that only designated users have access to the apps you create using this workaround.

A Marketing Cloud app is an externally hosted application that is iframed into Marketing Cloud. Marketing Cloud apps include custom apps built by your organization or apps installed from AppExchange. You launch an app via the Marketing Cloud app menu:

A common Marketing Cloud app you might be familiar with is Query Studio for Salesforce Marketing Cloud:

Query Studio

Query Studio is an externally hosted app, which lets you write and run SQL queries and instantly see the query results onscreen, with a similar experience to SQL Server Studio or MySQL workbench.

Another example of a Marketing Cloud app that you might be familiar with is Deployment Manager for Marketing Cloud:

Deployment Manager

Deployment Manager lets you import and export Marketing Cloud Configuration and easily distribute it to other Marketing Cloud Enterprises and Business Units.

Both those apps, Query Studio and Deployment Manager, have been developed by Salesforce Labs and are publicly available to download for free from AppExchange.

There are many more commercial apps for Marketing Cloud in AppExchange, all of them hosted externally. This means that for security, they all must use a web app or public app OAuth 2.0 integration to acquire an access token, and use that access token to request information about the end-user by calling the v2/userinfo REST endpoint.

Here’s a brief overview of creating an externally hosted Marketing Cloud app:

  1. Create an installed package, or navigate to an existing package.
  2. Under Components, click Add Component.
  3. Select Marketing Cloud App.
  4. Enter a name and description for your app.
  5. Enter your app’s login, redirect, and logout URLs. Point to localhost or test locations first, if needed, and edit these values later. All URLs must be HTTPS (TLS).
    • Login – Marketing Cloud uses this endpoint to iframe your externally hosted app. Your app can show anything here. Your app must set a cookie at login. To retrieve information about the end user, ensure that your externally hosted app immediately kicks off Marketing Cloud’s OAuth 2.0 authorization code flow and then calls the v2/userinfo route after calling your login endpoint. Legacy packages only: Marketing Cloud posts the JWT here.
    • Logout – Marketing Cloud performs a GET on the logout endpoint from the browser. This logout URL ends the user’s session and unsets the cookie set on login. When the user logs out of Marketing Cloud, the app session also ends.
  6. Save the component.
  7. Log out of Marketing Cloud, and log back in to see your app in the AppExchange menu in Marketing Cloud.

Source: Create a Marketing Cloud App

But what if you cannot, or don’t even want to build an elaborate, externally hosted app? If you’re looking for a simple, yet elegant solution that will only be used internally by you and your colleagues, hosting your app in Salesforce Marketing Cloud’s Web Studio might be the workaround that will fulfil your requirements.

My teams have built numerous Marketing Cloud apps that way. To give you some examples of what we used them for:

  • monitoring all journeys in the account,
  • monitoring all automations in the account,
  • creating robust email tracking dashboards,
  • creating functionalities to help users get their work done faster and with less manual steps.

Let’s get to it!

Create an app on a CloudPage

Let’s start by creating a simple app on a CloudPage. We can use an app I already described on this blog in the article called Find a Data Extension and it’s folder path using SSJS. Here is the full script you will need for your CloudPage:

<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>
Find a Data Extension and it’s folder path using SSJS

Now you can publish the CloudPage and test if it works correctly before we move on to the next step.

Create a Marketing Cloud App

Let’s now move on to the main Setup and in there to Platform Tools > Installed Packages. On the page with All Packages click on New to add a new package. Give the package a name and describe it’s function (note, that this name and description will only be visible to administrators who have access to setup in Salesforce Marketing Cloud).

In the Components section, click on Add Component and chose a Marketing Cloud App:

Now you will need to Set Marketing Cloud App Properties:

  • Name – the name of your app which will be visible for all users
  • Description – description of what your app does
  • Login Endpoint – the external URL of the CloudPage you created and published earlier, eg. https://pub.s10.exacttarget.com/xzy
  • Logout Endpoint – use the same URL as above

Once your app is saved, log out of Marketing Cloud and log back in, to see your app in the main menu, under the AppExchange icon, from where you can launch it:

Access and Sharing

Just like with all enhanced packages, you can license packages installed from parent business units to other business units across your enterprise. On top of that, you can choose which users should be able to access the app and from which BUs. This means that in a few easy clicks, you can either:

  • give access to the app to all users in all Business Units,
  • choose certain Business Units, where all existing and future users of those BUs would be granted access to the app,
  • or pick your designated users and choose in which Business Units they should be able to access the app.

Here’s a step-by-step guide on Licensing for Enhanced Packages:

Installed Packages in Marketing Cloud
Manage Licensing for Installed Packages
  1. From the detailed view for any package, click the Access tab to manage user licensing.
  2. Search for a business unit to grant or restrict licensing for the package. If you have only one business unit, the search field and tree is hidden.
  3. License specific users in the selected business unit. When users are added, add licensing for those users here.
  4. For server-to-server integrations only: To assign licenses to users or to make API requests on behalf of this business unit, enable the package’s server-to-server integration for that business unit. You can assign licenses only if the package contains another component in addition to the server-to-server API integration, which isn’t licensable. Enable Business Units isn’t shown for other integration types.
  5. License all current and future users in the business unit.
  6. For server-to-server integrations only: Enable the package’s server-to-server integration for all business units in your account. You can assign licenses to users in all business accounts and make API requests on behalf of all business units in your account. This option isn’t shown for other integration types.
  7. License all current and future users for all business units in your account. If this option isn’t shown, you don’t have permission to administer installed packages in all business units in your account. To gain access, work with your account administrator.

Make your users feel at home

If the app you’re planning to create will be processing a lot of data or multiple requests that might take some time, you can add a Salesforce-style spinner that your users are familiar with.

Now you’re all set, so have fun with creating your own app!


Questions? Comments?

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