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

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

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

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

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

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

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

Authentication

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

JSON Parameters

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

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

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

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

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


Questions? Comments?

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

Working with Salesforce Multi-Select Picklists in AMPscript

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

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

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

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

  • Events
  • Newsletters
  • Promotions
  • Special Offers
  • Surveys

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

Splitting a string by a delimiter

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

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

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

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

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

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

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

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

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

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

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

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

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

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

HTML checkboxes and AMPscript

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

<input type="checkbox" checked>

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

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

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

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

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

Update Multi-Select Picklist values in Salesforce using AMPscript

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

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

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

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

The full script

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

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

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


Questions? Comments?

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

Troubleshooting Marketing Cloud Connect Business Unit mapping

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

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

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

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

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

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

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

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

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


Questions? Comments?

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

Customize your Marketing Cloud account with your company’s branding

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

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

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

Create a new brand in your account

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

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

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

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

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

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

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

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

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


Questions? Comments?

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

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

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

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

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

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

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

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

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

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

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

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

Conslusion

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


Questions? Comments?

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

Exception handling in Salesforce Marketing Cloud CloudPages

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

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

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

Try/catch statement

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

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

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

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

Log all caught errors

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

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

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

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

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

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

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

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


Questions? Comments?

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

Inventory of Data Extensions and their data sources

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

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

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

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

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

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

Here’s the Data Extension structure you will need:

Retrieve the ImportDefinition data

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

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

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

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

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

Retrieve the QueryDefinition data

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

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

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

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

The full script

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

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

Questions? Comments?

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

Simple Marketing Cloud App hosted on a CloudPage

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

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

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

Query Studio

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

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

Deployment Manager

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

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

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

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

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

Source: Create a Marketing Cloud App

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

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

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

Let’s get to it!

Create an app on a CloudPage

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

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

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

Create a Marketing Cloud App

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

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

Now you will need to Set Marketing Cloud App Properties:

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

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

Access and Sharing

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

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

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

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

Make your users feel at home

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

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


Questions? Comments?

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

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.