How to create a listless double opt-in in Marketing Cloud with AMPscript

How to create a listless double opt-in in Marketing Cloud with AMPscript

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
Email EmailAddress truetrue
Opt-In Boolean false

Tokens

Name FieldType IsPrimaryKey IsRequired MaxLength
Email EmailAddress truetrue254
Token Text500
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 truetrue
EmailAddress EmailAddress 254
TokenText500

Email

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

Pay me a coffee

Want to say thanks? Pay me a coffee! Remember, I turn coffee into code.

Leave a Reply

Your email address will not be published. Required fields are marked *

server-side Javascript
Up Next:

How to clone a Data Extension and its records with server-side Javascript

How to clone a Data Extension and its records with server-side Javascript