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.

Add subscribers to a Journey from a CloudPage using Fire Entry Event

Firing an Entry Event is great for injecting contacts into a Salesforce Marketing Cloud journey, especially if you need to be able to inject them from an external system or a website.

It’s also useful when you’re creating a custom form on a CloudPage and would like to replicate the behavior of SmartCapture forms, which allow injecting contacts to a journey upon form submission (real-time).

In order to use the Fire Entry Event on a CloudPage, we will build a simple form to capture the end user’s email address and we will use REST API to post the collected information to /interaction/v1/events route using Server-Side JavaScript. But first, let’s start with creating a new journey in Salesforce Marketing Cloud’s Journey Builder.

API Event Entry Source

When you create the journey for use with a custom form on a CloudPage, the only difference in the setup is the entry source, which in this case will be the API Event.

The API event in Journey Builder connects the journey canvas to an API used to admit contacts into a journey. When the API fires an event, the contacts entering the journey are stored in a Marketing Cloud data extension you choose. You can set a filter using Marketing Cloud data attributes to ensure that only intended customers enter the journey.

When you drag and drop the API Event into your journey’s canvas, click on it and choose to create an event. You will be prompted to choose a Data Extension for use with the journey and you will also see that an Event Definition Key has been created for your journey – copy it, as we will need it later for our script. Activate the journey.

Custom form on a CloudPage

We can now create our form on a CloudPage. For the purpose of this tutorial, it will be a very simple form collecting just the end user’s email address. We will also add some AMPscript to process the data from the form. If you haven’t created a form on a CloudPage yet, you might want to check out this article first: Create a Sales Cloud-integrated lead capture form using AMPscript. Here’s a basic form for our use case:

%%[
SET @Submit = RequestParameter("Submit")
IF @Submit != "Success" THEN
]%%
<h2>Email:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="GET">
<label for="email">Email address:</label>
<input type="email" id="email" name="email" required="yes">
<button type="submit" name="Submit" id="Submit" value="Success">Submit</button>
</form>
%%[
ENDIF
]%%
%%[
SET @email = RequestParameter("email")
IF @email != "" THEN
]%%
<! –– fire entry event script placeholder ––>
%%[
ENDIF
]%%
view raw simple-form-FEE.html hosted with ❤ by GitHub

Fire an Entry Event using API

In order to inject a contact from our form into a journey, we will use Salesforce Marketing Cloud’s REST API, specifically the /interaction/v1/events route. Here’s an example request:

Host: https://YOUR_SUBDOMAIN.rest.marketingcloudapis.com
POST /interaction/v1/events
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"ContactKey": "ID601",
"EventDefinitionKey":"AcmeBank-AccountAccessed",
"Data": {
"accountNumber":"123456",
"patronName":"John Smith" }
}

And an example response, which would indicate that the request has been processed correctly:

HTTP/1.1 201
{
"eventInstanceId": "########-####-####-####-############"
}

Like with any other API call, we will need to start with authentication. If you are not familiar with the concept, check out my other article, Salesforce Marketing Cloud API Authentication using Server-Side JavaScript.

As a security measure, it’s best to store your Client Id and Secret as encoded values in a Data Extension to avoid exposing them in your script. My preferred way is to use the EncryptSymmetric and DecryptSymmetric AMPscript functions for encryption, and a simple lookup to get their values from a Data Extension.

Here’s how the authentication part could look like:

//authenticate to get access token
var authEndpoint = 'https://mcxxx.auth.marketingcloudapis.com/&#39; //provide API endpoint
var payload = {
client_id: "xxxxxx", //pass Client ID
client_secret: "xxxxxx", //pass Client Secret
grant_type: "client_credentials"
};
var url = authEndpoint + '/v2/token'
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.access_token
var rest_instance_url = tokenResponse.rest_instance_url
};

Once we obtain the access_token and the rest_instance_url, we can move on to our actual call. We will pass the access_token in the authentication header, and in the payload, we will pass the data collected through the form and our journey’s event definition key. To get the form data, we will use an undocumented Server-Side JavaScript function, Request.GetQueryStringParameter(). Remember to include all the values passed in the payload in your target Data Extension, which you chose earlier upon creating the journey. Here we have just one such field, called email_field:

//make api call to inject contact to a journey
if (email != null && accessToken != null) {
var email = Request.GetQueryStringParameter("email");
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var jsonBody = {
"ContactKey": email,
"EventDefinitionKey": "APIEvent-xxxxxx", //provide journey EVENT DEFINITION KEY
"Data": {
"email_field": email
}
};
var requestUrl = rest_instance_url + "/interaction/v1/events";
var fireEntryEvent = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);

The full script

If you put all the above together, you should end up with something like this:

%%[
SET @Submit = RequestParameter("Submit")
IF @Submit != "Success" THEN
]%%
<h2>Email:</h2>
<form action="%%=RequestParameter('PAGEURL')=%%" method="GET">
<label for="email">Email address:</label>
<input type="email" id="email" name="email" required="yes">
<button type="submit" name="Submit" id="Submit" value="Success">Submit</button>
</form>
%%[
ENDIF
]%%
%%[
SET @email = RequestParameter("email")
IF @email != "" THEN
]%%
<script runat="server">
Platform.Load("Core", "1");
try {
//authenticate to get access token
var authEndpoint = 'https://xxxx.auth.marketingcloudapis.com/&#39; //provide API endpoint
var payload = {
client_id: "xxxxxx", //pass Client ID
client_secret: "xxxxxx", //pass Client Secret
grant_type: "client_credentials"
};
var url = authEndpoint + '/v2/token'
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.access_token
var rest_instance_url = tokenResponse.rest_instance_url
};
//make api call to inject contact to a journey
if (email != null && accessToken != null) {
var email = Request.GetQueryStringParameter("email");
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var jsonBody = {
"ContactKey": email,
"EventDefinitionKey": "APIEvent-xxxxxxx", //provide journey EVENT DEFINITION KEY
"Data": {
"email_field": email
}
};
var requestUrl = rest_instance_url + "/interaction/v1/events";
var fireEntryEvent = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);
if (fireEntryEvent.StatusCode == 201) {
Write("Success");
}
};
} catch (error) {
Write("Error");
}
</script>
%%[
ENDIF
]%%

If you would like to see this script in action, enter your email address on a demo CloudPage I set up: https://pub.s10.exacttarget.com/hkphfaftk4b


Questions? Comments?

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

Salesforce-style spinner for use on CloudPages

Spinners are loading indicators that can be shown when retrieving data or performing other operations.

We will use a common spinner that Salesforce Lightening users are familiar with – the full documentation can be found here. This spinner is a Lightning component, which means that it cannot be directly used on a CloudPage. My awesome colleague Anna rewrote it into HTML and CSS, so that it can be easily copied and pasted for use on a CloudPage. This way, you can emulate the look and feel of other Salesforce clouds on your CloudPages.

It doesn’t necessarily add any value for external users of your CloudPages, but it can improve the experience of Salesforce Marketing Cloud users who internally use apps created on CloudPages and are familiar with other Salesforce clouds.

Here’s a preview of the spinner we will create in a few easy steps:

Let’s start with adding the CSS for our spinner. You can paste below code anywhere in your existing style-sheet, or create a separate one and link to it in the document head:

.slds-spinner_container{position:absolute;top:0;right:0;bottom:0;left:0;z-index:9050;background-color:hsla(0,0%,100%,.75);visibility:visible;opacity:1;transition:opacity .2s ease,visibility 0;transition-delay:0s,.3s;display:none}
.slds-spinner{position:absolute;top:50%;left:50%;z-index:9051;transform:translate(-50%,-50%) rotate(90deg)}
.slds-spinner,.slds-spinner__dot-a,.slds-spinner__dot-b{transform-origin:50% 50%;will-change:transform}
.slds-spinner__dot-a,.slds-spinner__dot-b{position:absolute;top:0;left:0;width:100%}
.slds-spinner:after,.slds-spinner:before,.slds-spinner__dot-a:after,.slds-spinner__dot-a:before,.slds-spinner__dot-b:after,.slds-spinner__dot-b:before{content:"";position:absolute;background:#b0adab;border-radius:50%;animation-duration:1s;animation-iteration-count:infinite;transform:translateZ(0)}
.slds-spinner__dot-a{transform:rotate(60deg)}
.slds-spinner__dot-b{transform:rotate(120deg)}
.slds-spinner:before{animation-delay:-.083s}
.slds-spinner__dot-a:before{animation-delay:.083s}
.slds-spinner__dot-b:before{animation-delay:.25s}
.slds-spinner:after{animation-delay:.41666667s}
.slds-spinner__dot-a:after{animation-delay:.583s}
.slds-spinner__dot-b:after{animation-delay:.75s}
@keyframes dotsBounceBefore-medium {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(-.5rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateZ(0)}
}
@keyframes dotsBounceAfter-medium {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(.5rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateX(0)}
}
.slds-spinner–large,.slds-spinner_large{width:2.75rem}
.slds-spinner–large .slds-spinner__dot-a:after,.slds-spinner–large .slds-spinner__dot-a:before,.slds-spinner–large .slds-spinner__dot-b:after,.slds-spinner–large .slds-spinner__dot-b:before,.slds-spinner–large.slds-spinner:after,.slds-spinner–large.slds-spinner:before,.slds-spinner_large .slds-spinner__dot-a:after,.slds-spinner_large .slds-spinner__dot-a:before,.slds-spinner_large .slds-spinner__dot-b:after,.slds-spinner_large .slds-spinner__dot-b:before,.slds-spinner_large.slds-spinner:after,.slds-spinner_large.slds-spinner:before{width:.625rem;height:.625rem}
.slds-spinner–large .slds-spinner__dot-a:before,.slds-spinner–large .slds-spinner__dot-b:before,.slds-spinner–large.slds-spinner:before,.slds-spinner_large .slds-spinner__dot-a:before,.slds-spinner_large .slds-spinner__dot-b:before,.slds-spinner_large.slds-spinner:before{animation-name:dotsBounceBefore-medium;top:-.3125rem;left:-.3125rem}
.slds-spinner–large .slds-spinner__dot-a:after,.slds-spinner–large .slds-spinner__dot-b:after,.slds-spinner–large.slds-spinner:after,.slds-spinner_large .slds-spinner__dot-a:after,.slds-spinner_large .slds-spinner__dot-b:after,.slds-spinner_large.slds-spinner:after{animation-name:dotsBounceAfter-medium;top:-.3125rem;right:-.3125rem}
@keyframes dotsBounceBefore-large {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(-.75rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateX(0)}
}
@keyframes dotsBounceAfter-large {
0%{transform:translateZ(0)}
60%{transform:translateZ(0);animation-timing-function:cubic-bezier(.55,.085,.68,.53)}
80%{transform:translate3d(.75rem,0,0);animation-timing-function:cubic-bezier(0,1.11,.7,1.43)}
to{transform:translateX(0)}
}
view raw spinner-styles.css hosted with ❤ by GitHub

Now let’s add our spinner to the CloudPage. We will create an HTML button and add an onclick event, so that the spinner activates once the user clicks the button:

<button onClick="showSpinner()">Try me!</button>
<div class="slds-spinner_container">
<div role="status" class="slds-spinner slds-spinner_large"><span class="slds-assistive-text"></span>
<div class="slds-spinner__dot-a"></div>
<div class="slds-spinner__dot-b"></div>
</div>
</div>
view raw spinner-html.html hosted with ❤ by GitHub

And finally, let’s add the script with the showSpinner() function:

<script>
function showSpinner() {
$spinner = document.getElementsByClassName("slds-spinner_container")[0];
$spinner.style.display = "block";
}
</script>
view raw spinner-js.html hosted with ❤ by GitHub

Above function will activate the spinner once the button is clicked and it will run indefinitely, which means that it’s appropriate for any use cases where the website gets reloaded or redirected to another one after all operations have been completed.

If you would like to control for how long the spinner is displayed, you can use the setTimeout method to deactivate the spinner after a given time. Below script will hide the spinner after 5 seconds:

<script>
function showSpinner() {
$spinner = document.getElementsByClassName("slds-spinner_container")[0];
$spinner.style.display = "block";
setTimeout(() => {
$spinner.style.display = "none";
}, 5000);
}
</script>

You can also control the activation/deactivation of the spinner by adding a hideSpinner() function to other functions used for performing operations. Below example will post data to another website using the fetch method and hide the spinner after the operation is complete:

<script>
function showSpinner() {
$spinner = document.getElementsByClassName("slds-spinner_container")[0];
$spinner.style.display = "block";
}
function hideSpinner() {
$spinner.style.display = "none";
}
...
function postData() {
fetch("http://example.com/myData.json&quot;, {
method: "POST",
headers: {
},
body: ({
})
}).then(function(res) {
hideSpinner(), window.alert("Success")
}).catch(function(err) {
hideSpinner(), window.alert("Error")
})
}
</script>
view raw hideSpinner.html hosted with ❤ by GitHub

To see the spinner in action, visit my demo CloudPage: https://pub.s10.exacttarget.com/c5luwxbwtxa.


Questions? Comments?

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

Create a CloudPages form with an image/file upload option

The HTML <input> tag specifies an input field where the user can enter data. An input field can vary in many ways, depending on the type attribute. The <input> elements with type="file" let the user choose one or more files from their device storage. The file can then be manipulated using JavaScript in order to process it further.

In this article, we will focus on creating a CloudPages form with an image input field, and uploading that image into Marketing Cloud’s Content Builder. The high-level outline of this process, which can be found in this Stack Exchange post, is: once the file has been uploaded via the form, it needs to be Base64 encoded, and then passed in a REST API call to the /asset/v1/content/assets route in order to create the image in Content Builder.

We will need to create two separate CloudPages: the first one will run client-side JavaScript in order to encode the file into a Base64 string and it will then pass it to the second CloudPage, which will run server-side JavaScript to make an API call to create a new asset in Content Builder.

I will break it down into smaller parts to make it easier to understand the different steps along the way.

Create an input field

Let’s start with creating an <input> field on a CloudPage. The input needs to be of the type="file" in order to allow the end-user to choose a file for upload from their local machine. We will also add an accept attribute, which specifies a filter for what file types the user can pick from the file input dialogue box. For our use case, it will be image/*: [see code snippet]

Please select a file to upload:
<br>
<input id="file" type="file" accept="image/*">
<br>
<button id="button">Upload</button>
view raw image-input.html hosted with ❤ by GitHub

We also need to include an id attribute in order to be able to reference the input field in JavaScript.

Encode the image using Base64

Base64 is a group of binary-to-text encoding schemes which enable storing and transferring data over media that are designed to deal with ASCII (text) only. In order to encode the image, we will use the FileReader.readAsDataURL() method to securely read the contents of a file stored on the user’s computer and return a result attribute containing a data: URL representing the file’s data. We will use the addEventListener() method to attach a click event to the Upload button, which will trigger the encoding function: [see code snippet]

document.getElementById('button').addEventListener('click', function() {
var files = document.getElementById('file').files;
if (files.length > 0) {
getBase64(files[0]);
}
});
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
console.log(reader.result);
};
reader.onerror = function (error) {
console.log('Error: ', error);
};
}

The format of the returned result is the following:

data:[<mediatype>][;base64],<data>

The mediatype is a MIME type string, such as ‘image/jpeg’ for a JPEG image file and <data> is the Base64 encoded string representing the uploaded file.

Fetch the data to processing page

We now need to pass the data onto our second CloudPage for further processing. The reason why we are using two pages instead of one is that client-side JavaScript is run after the server-side is complete, which makes it impossible to run them in reverse order on the same page.

In order to pass the data between our CloudPages, we will use the fetch() method. But first, let’s prepare the data that we will need for our API call: [see code snippet]

var fileEncoded = reader.result;
var base64enc = fileEncoded.split(";base64,")[1];
var fullFileName = document.getElementById("file").files[0].name;
var fileName = fullFileName.split(".")[0];
var assetName = fullFileName.split(".")[1];
  • base64enc is the encoded string which represents the image
  • fullFileName is the full name of the file uploaded by the user (eg. “astro.png”)
  • fileName is the first part of the file name, before the filename extension (eg. “astro”)
  • assetName is the filename extension (eg. “png”)

Once we have the above, we can POST it in JSON format to the processing page that will contain the server-side script. Our request will look like this: [see code snippet]

fetch("https://pub.s10.exacttarget.com/xxxxxxx&quot;, { //provide URL of the processing page
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
base64enc: base64enc,
fileName: fileName,
assetName: assetName
})
})
.then(function(res) {
window.alert("Success!");
})
.catch(function(err) {
window.alert("Error!");
});
view raw fetch-data.js hosted with ❤ by GitHub

where the method is POST, headers specify that the content-type is JSON and in the JSON body we will have three attribute-value pairs, base64enc, fileName and assetName.

The fetch() method argument is the path to the CloudPage containing our server-side script, so, for now, you can create an empty CloudPage just to obtain the link.

The fetch() method returns a promise that resolves to a response to that request, whether it is successful or not: .then(function(res) / .catch(function(err).

The full client-side script

Let’s now put all of the above elements together to create our first CloudPage. The complete code will look like this: [see code snippet]

Please select a file to upload:
<br>
<input id="file" type="file" accept="image/*">
<br>
<button id="button">Upload</button>
<script runat="client">
document.getElementById("button")
.addEventListener("click", function() {
var files = document.getElementById("file").files;
if (files.length > 0) {
getBase64(files[0]);
}
});
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
//prepare data to pass to processing page
var fileEncoded = reader.result;
var base64enc = fileEncoded.split(";base64,")[1];
var fullFileName = document.getElementById("file").files[0].name;
var fileName = fullFileName.split(".")[0];
var assetName = fullFileName.split(".")[1];
fetch("https://pub.s10.exacttarget.com/xxxxxxxxx&quot;, { //provide URL of the processing page
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
base64enc: base64enc,
fileName: fileName,
assetName: assetName
})
})
.then(function(res) {
window.alert("Success!");
})
.catch(function(err) {
window.alert("Error!");
});
};
reader.onerror = function(error) {
console.log('Error: ', error);
};
}
</script>

Now we can move on and prepare the second part of the script.

Retrieve the posted data

In order to retrieve the data posted from our first CloudPage, we will use the getPostData() server-side JavaScript method, which sadly is not documented in the official Server-Side JavaScript Syntax Guide, but you can read about it in this post on Salesforce Stack Exchange. Once we access the body of our request, we can parse the data using the ParseJSON() function: [see code snippet]

//retrieve posted data
var jsonData = Platform.Request.GetPostData();
var obj = Platform.Function.ParseJSON(jsonData);
view raw get-posted-data.js hosted with ❤ by GitHub

Prepare the data for API call

Now we can prepare the data for our API call. Let’s take a look at an example JSON body of a create asset request: [see code snippet]

POST /asset/v1/content/assets
{
"name": "Astro",
"assetType": {
"name": "png",
"id": 28
},
"file": "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"
}

In the above, the first name represents the name of the file. The second parameter assetType, consists of a name which represents the file extension and an id of the asset type. Asset types are the file or content types supported by Content Builder and the full list can be found here: List of Asset Types.

Let’s prepare the data and match the asset type based on the name of the file extension: [see code snippet]

//prepare data for API call
var base64enc = obj.base64enc;
var fileName = obj.fileName;
var assetName = obj.assetName;
//match asset type with uploaded file (https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/base-asset-types.htm)
var assetTypes = { ai: 16, psd: 17, pdd: 18, eps: 19, gif: 20, jpe: 21, jpeg: 22, jpg: 23, jp2: 24, jpx: 25, pict: 26, pct: 27, png: 28, tif: 29, tiff: 30, tga: 31, bmp: 32, wmf: 33, vsd: 34, pnm: 35, pgm: 36, pbm: 37, ppm: 38, svg: 39, "3fr": 40, ari: 41, arw: 42, bay: 43, cap: 44, crw: 45, cr2: 46, dcr: 47, dcs: 48, dng: 49, drf: 50, eip: 51, erf: 52, fff: 53, iiq: 54, k25: 55, kdc: 56, mef: 57, mos: 58, mrw: 59, nef: 60, nrw: 61, orf: 62, pef: 63, ptx: 64, pxn: 65, raf: 66, raw: 67, rw2: 68, rwl: 69, rwz: 70, srf: 71, sr2: 72, srw: 73, x3f: 74, "3gp": 75, "3gpp": 76, "3g2": 77, "3gp2": 78, asf: 79, avi: 80, m2ts: 81, mts: 82, dif: 83, dv: 84, mkv: 85, mpg: 86, f4v: 87, flv: 88, mjpg: 89, mjpeg: 90, mxf: 91, mpeg: 92, mp4: 93, m4v: 94, mp4v: 95, mov: 96, swf: 97, wmv: 98, rm: 99, ogv: 100, indd: 101, indt: 102, incx: 103, wwcx: 104, doc: 105, docx: 106, dot: 107, dotx: 108, mdb: 109, mpp: 110, ics: 111, xls: 112, xlsx: 113, xlk: 114, xlsm: 115, xlt: 116, xltm: 117, csv: 118, tsv: 119, tab: 120, pps: 121, ppsx: 122, ppt: 123, pptx: 124, pot: 125, thmx: 126, pdf: 127, ps: 128, qxd: 129, rtf: 130, sxc: 131, sxi: 132, sxw: 133, odt: 134, ods: 135, ots: 136, odp: 137, otp: 138, epub: 139, dvi: 140, key: 141, keynote: 142, pez: 143, aac: 144, m4a: 145, au: 146, aif: 147, aiff: 148, aifc: 149, mp3: 150, wav: 151, wma: 152, midi: 153, oga: 154, ogg: 155, ra: 156, vox: 157, voc: 158, "7z": 159, arj: 160, bz2: 161, cab: 162, gz: 163, gzip: 164, iso: 165, lha: 166, sit: 167, tgz: 168, jar: 169, rar: 170, tar: 171, zip: 172, gpg: 173, htm: 174, html: 175, xhtml: 176, xht: 177, css: 178, less: 179, sass: 180, js: 181, json: 182, atom: 183, rss: 184, xml: 185, xsl: 186, xslt: 187, md: 188, markdown: 189, as: 190, fla: 191, eml: 192, text: 193, txt: 194, freeformblock: 195, textblock: 196, htmlblock: 197, textplusimageblock: 198, imageblock: 199, abtestblock: 200, dynamicblock: 201, stylingblock: 202, einsteincontentblock: 203, webpage: 205, webtemplate: 206, templatebasedemail: 207, htmlemail: 208, textonlyemail: 209, socialshareblock: 210, socialfollowblock: 211, buttonblock: 212, layoutblock: 213, defaulttemplate: 214, smartcaptureblock: 215, smartcaptureformfieldblock: 216, smartcapturesubmitoptionsblock: 217, slotpropertiesblock: 218, externalcontentblock: 219, codesnippetblock: 220, rssfeedblock: 221, formstylingblock: 222, referenceblock: 223, imagecarouselblock: 224, customblock: 225, liveimageblock: 226, livesettingblock: 227, contentmap: 228, jsonmessage: 230 };
var assetTypeID = assetTypes[assetName];

If you’re only going to upload images, you don’t need to include the whole list of asset types – the ones that start with “2” will be sufficient.

Create an asset using REST API

In order to make an API call using the /asset/v1/content/assets route, we first need to authenticate. You will need the Installed Package for API integration, the Client Id and Cient Secret. If you’re not sure how to authenticate programmatically in Salesforce Marketing Cloud, refer to my article, Salesforce Marketing Cloud API Authentication using Server-Side JavaScript.

As a security measure, it’s best to store your Client Id and Secret as encoded values in a Data Extension to avoid exposing them in your script. My preferred way is to use the EncryptSymmetric and DecryptSymmetric AMPscript functions for encryption, and a simple lookup to get their values from a Data Extension.

Here’s the code snippet for the authentication part:

<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>

Once we obtain the accessToken, we will need to include it in our final API call, along with the rest_instance_url: [see code snippet]

var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var jsonBody = {
"name": fileName,
"assetType": {
"name": assetName,
"id": assetTypeID
},
"file": base64enc
};
var requestUrl = rest_instance_url + "asset/v1/content/assets"
var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);

The above call will create an image in Content Builder’s main folder and return the following response: [see code snippet]

{
"id":xxx,
"customerKey":"xxx",
"objectID":"xxx",
"assetType":{
"id":23,
"name":"jpg",
"displayName":"Image"
},
"fileProperties":{
"extension":"jpg",
"fileSize":24328,
"fileCreatedDate":"xxx",
"width":700,
"height":421,
"publishedURL":"xxxx"
},
"name":"astro",
"owner":{
"id":xxx,
"email":"",
"name":"APIuser",
"userId":"xxx"
},
"createdDate":"xxx",
"createdBy":{
"id":xxx,
"email":"",
"name":"APIuser",
"userId":"xxx"
},
"modifiedDate":"xxx",
"modifiedBy":{
"id":xxx,
"email":"",
"name":"APIuser",
"userId":"xxx"
},
"enterpriseId":xxx,
"memberId":xxx,
"status":{
"id":2,
"name":"Published"
},
"category":{
"id":xxx8,
"name":"Content Builder",
"parentId":0
},
"availableViews":[
],
"modelVersion":2
}

The full server-side script

Here is the full script for our second CloudPage: [see code snippet]

<script runat="server">
Platform.Load("Core","1");
try {
//fetch posted data
var jsonData = Platform.Request.GetPostData();
var obj = Platform.Function.ParseJSON(jsonData);
//prepare data for API call
var base64enc = obj.base64enc;
var fileName = obj.fileName;
var assetName = obj.assetName;
//match asset type with uploaded file (https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/base-asset-types.htm)
var assetTypes = { ai: 16, psd: 17, pdd: 18, eps: 19, gif: 20, jpe: 21, jpeg: 22, jpg: 23, jp2: 24, jpx: 25, pict: 26, pct: 27, png: 28, tif: 29, tiff: 30, tga: 31, bmp: 32, wmf: 33, vsd: 34, pnm: 35, pgm: 36, pbm: 37, ppm: 38, svg: 39, "3fr": 40, ari: 41, arw: 42, bay: 43, cap: 44, crw: 45, cr2: 46, dcr: 47, dcs: 48, dng: 49, drf: 50, eip: 51, erf: 52, fff: 53, iiq: 54, k25: 55, kdc: 56, mef: 57, mos: 58, mrw: 59, nef: 60, nrw: 61, orf: 62, pef: 63, ptx: 64, pxn: 65, raf: 66, raw: 67, rw2: 68, rwl: 69, rwz: 70, srf: 71, sr2: 72, srw: 73, x3f: 74, "3gp": 75, "3gpp": 76, "3g2": 77, "3gp2": 78, asf: 79, avi: 80, m2ts: 81, mts: 82, dif: 83, dv: 84, mkv: 85, mpg: 86, f4v: 87, flv: 88, mjpg: 89, mjpeg: 90, mxf: 91, mpeg: 92, mp4: 93, m4v: 94, mp4v: 95, mov: 96, swf: 97, wmv: 98, rm: 99, ogv: 100, indd: 101, indt: 102, incx: 103, wwcx: 104, doc: 105, docx: 106, dot: 107, dotx: 108, mdb: 109, mpp: 110, ics: 111, xls: 112, xlsx: 113, xlk: 114, xlsm: 115, xlt: 116, xltm: 117, csv: 118, tsv: 119, tab: 120, pps: 121, ppsx: 122, ppt: 123, pptx: 124, pot: 125, thmx: 126, pdf: 127, ps: 128, qxd: 129, rtf: 130, sxc: 131, sxi: 132, sxw: 133, odt: 134, ods: 135, ots: 136, odp: 137, otp: 138, epub: 139, dvi: 140, key: 141, keynote: 142, pez: 143, aac: 144, m4a: 145, au: 146, aif: 147, aiff: 148, aifc: 149, mp3: 150, wav: 151, wma: 152, midi: 153, oga: 154, ogg: 155, ra: 156, vox: 157, voc: 158, "7z": 159, arj: 160, bz2: 161, cab: 162, gz: 163, gzip: 164, iso: 165, lha: 166, sit: 167, tgz: 168, jar: 169, rar: 170, tar: 171, zip: 172, gpg: 173, htm: 174, html: 175, xhtml: 176, xht: 177, css: 178, less: 179, sass: 180, js: 181, json: 182, atom: 183, rss: 184, xml: 185, xsl: 186, xslt: 187, md: 188, markdown: 189, as: 190, fla: 191, eml: 192, text: 193, txt: 194, freeformblock: 195, textblock: 196, htmlblock: 197, textplusimageblock: 198, imageblock: 199, abtestblock: 200, dynamicblock: 201, stylingblock: 202, einsteincontentblock: 203, webpage: 205, webtemplate: 206, templatebasedemail: 207, htmlemail: 208, textonlyemail: 209, socialshareblock: 210, socialfollowblock: 211, buttonblock: 212, layoutblock: 213, defaulttemplate: 214, smartcaptureblock: 215, smartcaptureformfieldblock: 216, smartcapturesubmitoptionsblock: 217, slotpropertiesblock: 218, externalcontentblock: 219, codesnippetblock: 220, rssfeedblock: 221, formstylingblock: 222, referenceblock: 223, imagecarouselblock: 224, customblock: 225, liveimageblock: 226, livesettingblock: 227, contentmap: 228, jsonmessage: 230 };
var assetTypeID = assetTypes[assetName];
//authenticate to get access token
var authEndpoint = 'https://xxxxxxxx.auth.marketingcloudapis.com/&#39; //add authentication endpoint
var payload = {
client_id: "xxxxx", //pass Client ID
client_secret: "xxxxx", //pass Client Secret
grant_type: "client_credentials"
};
var url = authEndpoint + '/v2/token'
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.access_token
var rest_instance_url = tokenResponse.rest_instance_url
}
//make api call to create asset
if (base64enc != null) {
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var jsonBody = {
"name": fileName,
"assetType": {
"name": assetName,
"id": assetTypeID
},
"file": base64enc
};
var requestUrl = rest_instance_url + "asset/v1/content/assets"
var createAsset = HTTP.Post(requestUrl, contentType, Stringify(jsonBody), headerNames, headerValues);
}
} catch (error) {
Write("<br>error: " + Stringify(error));
}
</script>

You can now go back to your first CloudPage and start uploading!

To see this script in action, visit my CloudPage.


Questions? Comments?

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

Exception handling in Salesforce Marketing Cloud emails

“Hope for the best and prepare for the worst” should be every marketer’s motto when it comes to creating contextual personalized content. Your company’s reputation could be easily tarnished if you send out buggy emails, that’s why exception handling should be an essential element of your email strategy.

In this article, you will learn about some of Salesforce Marketing Cloud’s build-in features and best practices that will help you maintain high quality of content that goes out to your subscribers.

Fallback values in emails

When it comes to creating personalized emails, we often hear Salesforce Marketing Cloud users assume that the data that comes through will be correct and that they are not going to include fallback values for their variables. The assumption should be exactly the opposite and you should always plan for bad or missing data!

If you’re using a Profile Attribute or a Data Extension column for personalizing an email, the best practice is to always leverage the Empty() AMPscript function to check whether there is a value available.

Here’s an example: it’s much easier to use a simple personalization string such as Dear %%FirstName%%, in your email, but if for some reason the data is missing, your subscriber would see Dear , in the email they receive. This can be easily prevented by using the Empty() function and adding a fallback value: [see code snippet]

%%[
set @firstName = AttributeValue("FirstName")
if empty(@firstName) then
set @greeting = "Valued Customer"
else
set @greeting = @firstName
endif
]%%
<p>Dear %%=v(@greeting)=%%,</p>

In the above example, a Profile Attribute or a Data Extension column will be used if there’s a value available, and if not, the greeting in the email will be set to Dear Valued Customer.

Whether you’re using Lists or Data Extensions to segment the data in Salesforce Marketing Cloud, you can also leverage the “Default Value” feature to ensure that required data is always available:

Profile Attribute with a default value
Data Extension field with a default value

Using the RaiseError function in emails

The RaiseError() function, used along with a conditional statement, ensures that the email does not get sent if there is bad or missing data in the dataset used for personalizing the email. Depending on how you set the function, it can either skip the send for a current subscriber and move on to next subscriber, or it can stop the remaining send job. Here’s a summary of the function’s main features in Eliot Harper’s video:

RaiseError AMPscript Function in Salesforce Marketing Cloud

Let’s take a look at the function syntax and properties:

RaiseError(1, 2, 3, 4, 5)

OrdinalTypeRequiredDescription
1stringNError message to display
2booleanYIndicates whether function skips send for current subscriber and continues or stops. A value of true skips the send for current subscriber and moves to next subscriber. A value of false stops the send and returns an error. Function defaults to false.
3stringNAPI error code (set by the user)
4stringNAPI error number (set by the user)
5booleanNIndicates whether the function records information to data extensions before error occurs, even if the process skips the subscriber. A value of 1 retains information written to data extensions before the error occurs, even if the subscriber is skipped. A value of 0 does not retain information recorded before the error. This parameter refers to inserted, updated, upserted, or deleted information via AMPscript.

Let’s use the previous script example with the FirstName personalization, but instead of setting a fallback value in case the FirstName is missing, we will use the RaiseError() function to skip the send for that particular subscriber: [see code snippet]

%%[
set @firstName = AttributeValue("FirstName")
if empty(@firstName) then
RaiseError("First name missing", true)
else
set @greeting = @firstName
endif
]%%
<p>Dear %%=v(@greeting)=%%,</p>

You will notice, that upon previewing the email, the error message that you set will be displayed if the FirstName value is missing:

In a scenario where you would try to send this email to a subscriber with a missing FirstName value, the function would prevent from sending an email to the current subscriber and would move on to the next one.

This function should not be misused for data segmentation. Even though a subscriber is skipped or the whole job is cancelled, it still counts towards your totals as a sent message, meaning that you will pay for each suppressed email as you would for a sent one. This is a small price to pay for handling exceptions and sustaining a positive reputation with your subscribers, but it should not replace regular data cleansing and maintenance processes.

An additional thing to keep in mind is that AMPscript is interpreted top-down. This means, that if there are any other AMPscript functions before the RaiseError() function, they will all be executed, even if the send for a particular subscriber is skipped in the end.

On the other hand, if you have set the RaiseError() function to cancel the remaining send job, then any Update, Insert, and Delete AMPscript functions, which are processed in bulk during send time, will not be executed, unless you specify otherwise by setting the last of the RaiseError() properties to 1.

Bear in mind, that the second property of the RaiseError() function, which indicates whether function skips send for the current subscriber and continues or stops, will default to false if not specified. This means, that unless set to true, it will always stop the remaining send job.

Logging RaiseError results

By default, the emails and jobs suppressed by the RaiseError() function will only be visible in the NotSent tracking extract. If you’re using this function in Journey Builder emails or Triggered Sends, you will also be able to see how many emails were suppressed by checking the “Errored” column in Email Studio’s Tracking tab, under Journey Builder Sends/Triggered Sends. Unfortunately, you won’t be able to view any details of the suppressed sends in the UI.

In order to log all suppressed sends, you can create a Data Extension and add an InsertDE function to the email. Make sure to set the last property of the RaiseError() function to 1 and to include the InsertDE() function before the RaiseError() function, as everything placed after won’t get executed. This way, each email suppressed by the RaiseError() function, will also be logged in the Data Extension: [see code snippet]

%%[
set @firstName = AttributeValue("FirstName")
if empty(@firstName) then
InsertDE("RaiseErrorLog", "JobID", jobid, "EmailName", emailname_, "SubscriberKey", _subscriberkey, "EmailAddress", emailaddr)
RaiseError("Missing first name", true, 1)
else
set @greeting = @firstName
endif
]%%
<p>Dear %%=v(@greeting)=%%,</p>

Analyzing the log will help you investigate why some of the values are missing and improve your company’s data hygiene.


Questions? Comments?

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