This article explains how to create a custom double opt-in with AMPscript in Salesforce Marketing Cloud, without using lists.
Not a fan of reading? Jump to the code snippet.
How it works
In order to create a double opt-in, we need to define how the opt-ins are created for every customer.
In this example, let us consider that creating an opt-in is just a matter of setting a field value from False to True in a Data Extansion called Consents.
Therefore, our first task would be creating a record in this Data Extension with the opt-in set to False by default.
Then, we will send an email with an activation link that would allow the customer to switch the opt-in from False to True. This link should be unique, secure and should have an expiration date of 15 minutes.
Data Extensions
We need to create 2 Data Extensions. The first one, Consents, will manage the opt-ins and the second one, Tokens, will cover the activation link and the link expiration date.
Both Data Extensions will be linked through the unique identifier of the customer, in this case Email.
Consents
Name | FieldType | IsPrimaryKey | IsRequired | DefaultValue |
EmailAddress | true | true | ||
Opt-In | Boolean | false |
Tokens
Name | FieldType | IsPrimaryKey | IsRequired | MaxLength |
EmailAddress | true | true | 254 | |
Token | Text | 500 | ||
CreatedDate | Date |
Triggered send
To create an Email with the activation link, we need to create a Triggered Send interaction.
As usual, to make it possible, we have to create a TriggeredSend Data Extension and an Email message.
After the Triggered Send has been created, we need to start it and take a note of the External Key (in this case 112233).
TriggeredSend Data Extension
Name | FieldType | IsPrimaryKey | IsRequired | MaxLength |
SubscriberKey | Text | true | true | |
EmailAddress | EmailAddress | 254 | ||
Token | Text | 500 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Confirmation Email</title>
</head>
<body>
<a href="%%=RedirectTo(CloudPagesURL(7788,'token',AttributeValue('Token')))=%%">Confirm your subscription</a>
<custom name="opencounter" type="tracking" />
</body>
</html>
Pages
In order to make our flow, we need to create 2 Cloud pages.
Form
The Form page (Page ID: 4455) will accommodate the subscription form and will send the data to the Form Handler page.
According to the action parameter present in the URL of the page, the Form will be hidden and a message will be shown instead.
%%[
SET @formHandlerPageId = 7788
SET @action = RequestParameter("action")
SET @pageUrl = RequestParameter('PAGEURL')
IF IndexOf(@pageUrl,'?') > 0 THEN
SET @pageUrl = Substring(@pageUrl,0,Subtract(IndexOf(@pageUrl,'?'),1))
ENDIF
]%%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Subscription Form</title>
<style>
.wrapper {
width:600px;
min-height: 300px;
margin: 5% auto 0;
}
</style>
</head>
<body>
<div class="wrapper">
<h1>Newsletter subscription.</h1>
%%[ IF @action == "confirm" THEN ]%%
<p>Please check your email.</p>
%%[ ELSEIF @action == "success" THEN ]%%
<p>Thank you for subscribing.</p>
<p><a href="%%=v(@pageUrl)=%%">Go back</a></p>
%%[ ELSEIF @action == "error" THEN ]%%
<p>Your token is missing or has expired.</p>
<p><a href="%%=v(@pageUrl)=%%">Please try again</a></p>
%%[ ELSEIF @action == "exist" THEN ]%%
<p>Your are already subscribed.</p>
<p><a href="%%=v(@pageUrl)=%%">Go back</a></p>
%%[ ELSE ]%%
<form method="POST" action="%%=CloudPagesURL(@formHandlerPageId)=%%">
<label for="Email">Your email:</label><br>
<input name="email" type="email" placeholder="ex: jon.snow@winterfell.co.uk">
<br>
<button>Subscribe me</button>
</form>
%%[ ENDIF ]%%
</div>
</body>
</html>
Form handler
The Form Handler page (Page ID: 7788) will handle 2 main scenarios.
The first one is what happens when the data is coming from the Form, and the second is what happens when the data is coming from the activation link.
In the first scenario, we generate a secret key (token), create a record in the Consents and Tokens, send the Email with the activation link and then redirect back to the Form with a confirm message.
We also need to check if the provided Email has his opt-in set to True already.
In the second scenario, we check if the token exists in the Tokens Data Extension and was created less than 15 minutes ago. If everything checks out, we update the opt-in from False to True in the Consents and redirect back to the Form with a success message.
%%[
SET @email = RequestParameter("email")
SET @token = RequestParameter("token")
SET @today = SystemDateToLocalDate(NOW())
SET @ts_externalKey = 112233
SET @formPageId = 4455
IF NOT EMPTY(@email) THEN
IF Lookup("Consents","Opt-In","Email",@email) == "True" THEN
Redirect(CloudPagesURL(@formPageId,"action","exist"))
ENDIF
SET @token = GUID()
UpsertDE("Consents",1,"Email",@email,"Opt-In","False")
UpsertDE("Tokens",1,"Email",@email,"Token",@token,"CreatedDate",@today)
IF NOT EMPTY(@ts_externalKey) THEN
SET @ts = CreateObject("TriggeredSend")
SET @ts_definition = CreateObject("TriggeredSendDefinition")
SetObjectProperty(@ts_definition, "CustomerKey", @ts_externalKey)
SetObjectProperty(@ts, "TriggeredSendDefinition", @ts_definition)
SET @ts_subscriber = CreateObject("Subscriber")
SetObjectProperty(@ts_subscriber, "EmailAddress", @email)
SetObjectProperty(@ts_subscriber, "SubscriberKey", @email)
SET @ts_token = CreateObject("Attribute")
SetObjectProperty(@ts_token, "Name", "Token")
SetObjectProperty(@ts_token,"Value", @token)
AddObjectArrayItem(@ts, "Attributes", @ts_token)
AddObjectArrayItem(@ts, "Subscribers", @ts_subscriber)
InvokeCreate(@ts, @statusMsg, @errorCode)
ENDIF
Redirect(CloudPagesURL(@formPageId,"action","confirm"))
ELSEIF NOT EMPTY(@token) THEN
SET @createdDate = Lookup("Tokens","CreatedDate","Token",@token)
SET @email = Lookup("Tokens","Email","Token",@token)
IF NOT EMPTY(@createdDate) AND @today < DateAdd(@createdDate, 15, "MI") THEN
UpdateData("Consents",1,"Email",@email,"Opt-In","True")
Redirect(CloudPagesURL(@formPageId,"action","success"))
ELSE
Redirect(CloudPagesURL(@formPageId,"action","error"))
ENDIF
ELSE
Redirect(CloudPagesURL(@formPageId,"action","error"))
ENDIF
]%%
Considerations
Note that we create links and redirects using the CloudPagesURL function.
Also, in this example, we use the GUID() function to generate the token, but feel free to use whatever method necessary to create a key which is unique.
Generate the Data Extensions
Please consider the following code for generating the Data Extensions for this project.
Note that in order to generate a TriggeredSend Data Extension, you need to know the Customer Key for the TriggeredSend Template. You can find it in the Marketing Cloud Contact Builder interface by choosing this template while creating a new Data Extension.
<script runat="server">
Platform.Load("core", "1.1");
var prox = new Script.Util.WSProxy();
var req1 = Folder.Retrieve({ Property: 'Name', SimpleOperator: 'equals', Value: 'MyFolder' });
var FolderID = req1[0].ID;
var dataExt1 = {
"CustomerKey": Platform.Function.GUID(),
"Name": "Consents",
"CategoryID": FolderID,
"Fields": [
{
"Name": "Email",
"FieldType": "EmailAddress",
"IsPrimaryKey": true,
"IsRequired": true
},
{
"Name": "Opt-In",
"FieldType": "Boolean",
"DefaultValue": false
}
]
}
var dataExt2 = {
"CustomerKey": Platform.Function.GUID(),
"Name": "Tokens",
"CategoryID": FolderID,
"Fields": [
{
"Name": "Email",
"FieldType": "EmailAddress",
"IsPrimaryKey": true,
"IsRequired": true,
"MaxLength": 254
},
{
"Name": "Token",
"FieldType": "Text",
"MaxLength": 500
},
{
"Name": "CreatedDate",
"FieldType": "Date"
}
]
}
var dataExt3 = {
"CustomerKey": Platform.Function.GUID(),
"Name": "TSD_Tokens",
"CategoryID": FolderID,
"IsSendable": true,
"Fields": [
{
"Name": "Token",
"FieldType": "Text",
"MaxLength": 500
}
],
"SendableSubscriberField": {
"Name": "_SubscriberKey"
},
"SendableDataExtensionField": {
"Name": "SubscriberKey"
},
"Template": {
"CustomerKey": "THIS-KEY-IS-UNIQUE-TO-YOUR-SYSTEM"
}
}
var response = prox.createBatch("DataExtension", [dataExt1, dataExt2, dataExt3]);
Write(Stringify(response));
</script>

Have I missed anything?
Please poke me with a sharp comment below or use the contact form.