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.
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]
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]
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.
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:
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]
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]
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]
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 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 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:
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.
Save the component.
Log out of Marketing Cloud, and log back in to see your app in the AppExchange menu in Marketing Cloud.
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.
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
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.
From the detailed view for any package, click the Access tab to manage user licensing.
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.
License specific users in the selected business unit. When users are added, add licensing for those users here.
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.
License all current and future users in the business unit.
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.
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!
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).
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:
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:
And an example response, which would indicate that the request has been processed correctly:
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:
The full script
If you put all the above together, you should end up with something like this:
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:
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:
And finally, let’s add the script with the showSpinner() function:
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:
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:
As a Salesforce Marketing Cloud developer, you have certainly been caught up in this loop: Develop the CloudPage > Save and Publish > Wait 2, 3 or even 5 minutes > Check the results. Moreover, if you are not debugging correctly, you can lose a lot of time between looking for the problem and waiting for your updates to propagate on your CloudPage. I must take this opportunity to invite you to check out Zuzanna’s article about debugging AMPScript in CloudPages, it is very helpful and well explained.
In this article, we will be covering two ways of testing your code on a CloudPage without having to wait for the propagation of changes every time we save and publish our work. These two ways are probably well know by experimented Salesforce Marketing Cloud developers, or by those who were fortunate enough to stumble upon some answers regarding this problem on Salesforce Stack Exchange.
Create an external web page
If you already have a web server, that is great. You can use it to create your web pages. However, if you are on a budget, or you just do not want to spend money on a hosting service, don’t worry, I have done the heavy lifting for you.
Create an account using one of the available options:
Step 2: Creating a free subdomain
Go to Hosting Tools > Domain Manager. Select “Create a free subdomain”, fill in the subdomain’s name, and click Create:
Step 3: Creating the web page
Go to Hosting Tools > File Manager. Double click on your subdomain’s folder:
Click on Create and check Create file. Give it a name like sfmc.html for example.
Step 4: add content and access the web page
Congrats, we have successfully created the web page. Double click on it to add the content. Click save when done editing The web page’s URL is as shown below without the /home/www/ part.
Call the page from within Salesforce Marketing Cloud
We are going to use two AMPScript functions to access our external page from within our CloudPage.
HTTPGet: Returns the content from our external web page. This function will only take our web page URL as a parameter.
TreatAsContent: Treats the content returned by the HTTPGet function as it came from a content area. This way, we make sure the AMPScript code in our web page gets evaluated.
For example, our CloudPage on Marketing Cloud should contain something like: [see code snippet]
Each time we need to update the code, we just have to update it on the external web page. No need to save and publish our CloudPage as its content will remain the same.
This way, we are avoiding the wait time between the publishing and the propagation of our changes on the servers when using a traditional development method.
Content Block By Key / Name / Id
The second method is using one of the three functions above. These functions return the content stored in the specified Content Block.
First, we will start by creating our Content Block. We need to go to Content Builder > Create > Content Blocks > Code Snippet.
I highly suggest using a Code Snippet instead of an HTML Content Block in order to avoid Content Builder’s editor from truncating parts of our HTML code.
Once we have created our Content Block and added our content in it. Time for getting it’s Key, Id or Name depending on which function we are using. We can find it by clicking on the drop-down icon and select Properties.
Now, we can head in to our CloudPage and add the code below: [see code snippet]
It pretty simple and straightforward. The code uses ContentBlockByKey to get our Content Block’s content. This is a onetime operation. Every time we need to update our code, we will only have to update it on the Content Block. This way, the updates are instantly live and we do not have to wait several minutes for the propagation on the servers.
Once we are done testing, we can copy the code on the external page or on the Content Block to our main CloudPage in Marketing Cloud and publish it.
About the author
Rachid Mamai is a SFMC geek and a Digital Marketing enthusiast living in France. To get in touch with Rachid, visit his LinkedIn.
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.
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]
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]
The format of the returned result is the following:
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
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]
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]
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]
Now we can move on and prepare the second part of the script.
Retrieve the posted data
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]
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]
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.
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:
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]
The above call will create an image in Content Builder’s main folder and return the following response: [see code snippet]
“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]
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:
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:
Let’s take a look at the function syntax and properties:
RaiseError(1, 2, 3, 4, 5)
Error message to display
Indicates 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.
API error code (set by the user)
API error number (set by the user)
Indicates 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]
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]
Analyzing the log will help you investigate why some of the values are missing and improve your company’s data hygiene.
In this tutorial, you will learn how to create a simple, yet fully customizable profile and subscription centre integrated with Sales and Service Cloud. Bear in mind, that the script utilizes a couple of standard and custom Sales/Service Cloud fields, which you will need to adjust based on your Salesforce CRM setup – the full list can be found here.
The profile center is a webpage where subscribers can enter and maintain the personal information that you keep about them. When you import a list, you can import attribute values for your subscribers that appear when a subscriber visits the profile center. The subscriber can update their information on this page and provide additional information.
A subscription center is a web page where a subscriber can control the messages they receive from your organization. The lists (including publication lists) you identify as public in the application are available for a subscriber to opt in to on the subscription center.
If you’re using Marketing Cloud Connect to integrate with Sales/Service Cloud, you can use the standard Profile Center based on the Marketing Cloud attributes that are mapped to fields in Sales or Service Cloud. Marketing Cloud Connect documentation states, that changes made in the Profile Center update Salesforce contact and lead data – unfortunately I haven’t been able to confirm this with my tests.
Another limitation which is very hard to overcome is the look and feel of the standard Profile and Subscription Center. If you enable Marketing Cloud BrandBuilder, you will be able to customize the color scheme of your Profile and Subscription Center based on the colors of the logo you upload – but you won’t be able to change the layout or the fonts used.
Sooner or later, most Marketing Cloud users switch to custom Profile and Subscription Centers, as the standard one is not enough when it comes to more complex use cases.
In the form, you can include any fields from any of the relevant Sales/Service Cloud objects, but to make it more simple as we go, for now, we will use three standard fields from the Contact object: FirstName, LastName and Email. Additionally, I have added three read-only checkboxes to the Contact object for subscription management: Newsletter, Offers and Events. [Why are they read-only? In this tutorial, you will learn how to allow subscribers to make changes to their preferences and sync those changes back to Salesforce. The process of managing the subscriptions by a Salesforce user and syncing the changes back to Marketing Cloud is a separate procedure and won’t be addressed in this article.] I also made sure that the Email Opt Out flag is visible on a Contact page layout in Sales Cloud:
Here is a reference of all the fields from the Contact object, which we will use in our script:
API Field Name
Email Opt Out
Let’s now create a simple HTML form which contains all of the above fields:
Since we want the form to be pre-populated with subscriber data from Sales/Service Cloud, we need to add the RetrieveSalesforceObjects function to pull the data, and display the retrieved data as field values:
You will also notice in the above script, that we have added a form action. The RequestParameter('PAGEURL') function reloads the page when the form is submitted, posting the form parameters back to the same page. You can, of course, post the form data to another page and process it there, but for the purpose of this tutorial, let’s keep everything on one page so that it’s easier to copy and paste.
After the form is submitted and posted, the data can be retrieved by using the RequestParameter AMPscript function. We will then update the data in Sales/Service Cloud using the UpdateSingleSalesforceObject function. Let’s add them now:
Congratulations – you just built the first, simple version of your profile and subscription center!
Now let’s analyze the above script. The first thing that you will notice is that all the AMPscript is placed at the beginning of the document, above the form – that’s because the form data first has to be posted before it can be processed, and upon posting the data, the page reloads and starts resolving the script from top-down. If you need more clarification on this concept, click here.
If the form has been submitted, we will update the Sales/Service Cloud contact with the new data that was posted upon form submission. The form data is retrieved using the RequestParameter function, which is used in the @updateRecord variable inside the UpdateSingleSalesforceObject function.
You will also notice, that for each of the checkboxes, I have added the following in-line IF function: Iif(RequestParameter("newsletter") == "on", "true", "false"). That is because the checkbox passed from the HTML form will have a value of either on or off, while to pass it to Sales/Service Cloud, we need to convert it to a boolean value of true or false.
Unsubscribe From All option
To make the functionality of our custom subscription and profile center similar to the standard one, we can add the Unsubscribe From All and Resubscribe options. The Unsubscribe From All option should not only set all the subscription-related flags in Sales Cloud to false, but also set the subscriber’s status to Unsubscribed in Salesforce Marketing Cloud’s All Subscribers list. To achieve this, we need to log an UnsubEvent. In order to do that, we will add an additional button to the form (Unsubscribe From All) and a hidden parameter which will be passed if someone clicks this button (name="unsub" type="hidden" value="true"). After submitting the form, if unsub value equals true, we will log the UnsubEvent in addition to updating the flags in Sales/Service Cloud. The below is a simplified version of logging an UnsubEvent and it will unsubscribe the contact from all BUs – if you would like to modify it to your use case, refer to this article: Unsubscribe and Log an UnsubEvent with a LogUnsubEvent Execute Call. Here’s what we need to add to our script to make this part work:
Since we have the option to Unsubscribe From All, we now also have to add an option to Resubscribe. The Resubscribe option will set all the subscription-related flags in Sales Cloud to true and set the subscriber’s status to Active in Salesforce Marketing Cloud’s All Subscribers list. The Resubscribe button will appear for any contact that has the HasOptedOutOfEmail flag set to true in Sales Cloud. The button also has a hidden parameter that will be passed with the form (name="sub" type="hidden" value="true"). Upon clicking the button, we will invoke the update method on a subscriber object to set their status to Active in Marketing Cloud, as well as update their subscription-related flags in Sales Cloud:
Update subscriber’s data in Marketing Cloud
If you’re using Marketing Cloud Connect, a very important thing to remember when updating subscriber’s email address is to do it in both, Sales/Service Cloud and Marketing Cloud. Here is why:
If an email address for a Lead or Contact Object is updated in Sales Cloud or Service Cloud, the corresponding email address is not updated in the All Subscribers list.
Eliot Harper, THE DATA HANDBOOK – Data Architecture for Salesforce Marketing Cloud
In order to achieve this, we will add one more piece of a script to update the subscriber object. First we will check, if the subscriber’s current email address in Marketing Cloud is different from the one provided in the form, and if it is, we will update it accordingly:
You do not need to include the above if you already have a process in place that synchronizes email address changes from Sales/Service Cloud to All Subscribers list in Salesforce Marketing Cloud. If you’d like to learn more about synching updates between the clouds, read Markus Slabina’s blog post here.
Custom subscription and profile center full script
As a final touch, to improve the experience, let’s add a confirmation message and a button that will reload the page and display the updated data in the form. Here is the final version of the script, with all of the above changes included:
Remember, that you will need to use the CloudPagesURL function to link to the subscription center CloudPage from an email.
In order to see how the above script works, feel free to play around with one of my test contacts in the test version of the subscription and profile center that I set up on a CloudPage: https://pub.s10.exacttarget.com/
Here are some resources if you would like to read further about subscription management best practices:
Here you will find an interesting concept of creating Campaigns for each subscription and adding the Contact as a Campaign Member
Here you can read how Effective Email Preference Centers Help Keep Subscribers Active and Engaged
In order to be able to programmatically trigger text messages, you will need to have MobileConnect enabled in Marketing Cloud’s Mobile Studio. You will also need to be able to create an installed package in your Marketing Cloud account to interact with Salesforce Marketing Cloud APIs. If you have both in place, we can start by creating a new SMS message.
Create a new message in MobileConnect
Go to Mobile Studio > Mobile Connect and click on Create Message. Choose Outbound and click on Next. In Message Setup, choose a Name, Short/Long Code to be used, From Name and choose API Trigger as the Send Method. Click on Next, type the Outbound Message text and choose the Next Keyword if needed. To finalize, click on Activate. Before you confirm, note the API Key displayed in the pop-up:
The message will now be visible in the Overview screen – make sure it’s status is Active/Scheduled.
Let’s start by installing a new, or identifying an existing installed package in your Marketing Cloud account. Note the Client Id and Client Secret as we will need them to authenticate our API request. Make sure that SMS is enabled in the scope of the package you are using:
Depending on the type of the package installed (v2 – enhanced functionality or v1 – legacy functionality), use one of the following code snippets to cover the authentication part of the script.
Below is an example messageContact API Request. The phone number passed in the payload must use the correct format for the designated country code. For example, a mobile number from the United States must include the numerical country code 1 and the area code, eg: 13175551212. [see code snippet on Github]
To ensure that the mobile number exists for the contact and that the contact subscribed to the specified keyword on your short code, set the Subscribe and Resubscribe values to true and specify the keyword parameter. If you’re not sure how to work with keywords, check out this help document: Keywords and Codes.
Now, let’s combine the first script snippet we used to authenticate with the messageContact payload and make a POST request. Below will work with the v1 legacy package: [see code snippet on Github]
In the above script, you will need to provide your endpoints, ClientId and ClientSecret (all three can be found in Setup > Apps > Installed Packages). You will also need to insert the message Api Key, the keyword and pass the recipient’s phone number.
Last but not least, always remember to put security measures into practice when setting up this kind of functionality on a CloudPage to prevent your ClientId and ClientSecret from being exposed.