James Lamb - Subscriber Preferences Management
Published on 06/06/2020
Everywhere I’ve worked, Subscriber Management has been a complex beast. You’ve got multiple backend systems that know about a person, you want to make sure a person is empowered to control their subscription status (not just unsubscribe), you’ve got legal issues around compliance and you’ve got the technical challenges of bounces.
At each place I’ve worked, we’ve approached that challenge from a different angle with varying levels of success, but recently I had a chance to design a system from scratch. My solution is for our particular business objectives and based on U.S. law which is far more lax than some other countries. In the case of law compliance and procedures, always make sure to check with your company’s legal team. I wish I could share more detail (because I’m pretty proud of it), but there are proprietary/confidential details that I can’t share. So I’ll cover a simplified version of this:
One automation, 20 data extensions (less if you left off development and qa), 42 queries (again, less w/o dev and qa), 1 Data Extract, 1 File Transfer and 1 Import.
Salesforce Marketing Cloud receives inputs in the following manner:
Runs hourly
Query - get any new records in the past 24 hours from the _Unsubscribe Data Extension (target SuppressionMaster, update) where the record isn’t also in MarketingEmailElection in a subscribed state with a later timestamp.
SELECT
u.SubscriberKey as EmailAddress,
MAX(u.EventDate) as dteOptOut,
'_Unsubscribe' as strSource
FROM
[_Unsubscribe] u
WHERE
u.EventDate > DATEADD(HOUR, -25, GETDATE())
AND
NOT EXISTS (select 1 from [MarketingEmailElection] mee WHERE
mee.EmailAddress = u.SubscriberKey
AND u.EventDate < mee.dteUpdate and mee.blnOptIn = 1)
GROUP BY
u.SubscriberKey
Query - get any new records that were passed via the unsubscribe API (target: SuppressionMaster, update)
SELECT
s.Email as EmailAddress,
s.[Last Modified] as dteOptOut,
'suppression_prod' as strSource
FROM
[WebSuppression] s
WHERE
NOT EXISTS (select 1 from [MarketingEmailElection] mee WHERE
mee.EmailAddress = s.Email
AND s.[Last Modified] < mee.dteUpdate
and mee.blnOptIn = 1)
Query - clear WebSuppression (could explore using SSJS to remove all rows) (target: WebSuppression, OVERWRITE)
SELECT
EmailAddress as Email
FROM
[SuppressionMaster]
WHERE
EmailAddress = 'intentionally, this will never match anything@hello.com'
Query - get all the opt-outs from the opt-out form (target SuppressionMaster, update)
SELECT
eco.EmailAddress,
eco.[dteSubmitted] as dteOptOut,
'ExternalCloudpageOptout' as strSource
FROM
[ExternalCloudpageOptout] eco
WHERE
NOT EXISTS (select 1 from [MarketingEmailElection] mee WHERE
mee.EmailAddress = eco.EmailAddress AND eco.[dteSubmitted] < mee.dteUpdate
and mee.blnOptIn = 1)
Query - Clear optout form. (similar to query #3)
Query - get any new unsubscribed records from _Subscribers (All Subs). Target SuppressionMaster, update
SELECT
s.EmailAddress,
s.[DateUnsubscribed] as dteOptOut,
'_Subscribers' as strSource
FROM
[_Subscribers] s
WHERE
s.status = 'unsubscribed'
AND
NOT EXISTS (select 1 from [MarketingEmailElection] mee WHERE
mee.EmailAddress = s.EmailAddress AND s.[DateUnsubscribed] < mee.dteUpdate
and mee.blnOptIn = 1)
Query - get all the unsubscribes from MarketingEmailElection - target SuppressionMaster, update
SELECT
EmailAddress,
dteUpdate as dteOptOut
FROM
[marketing_email_election_prod]
WHERE
blnOptin = 0
Ok, at this stage “SuppressionMaster” now has all of the unsubscribes from all the different places you can unsubscribe. Now, we’re going to handle all of the new “subscribe” (yes) elections.
Query - get all the “yes” entries from the past 72 hours from MarketingEmailElection and store in MeeYes1, OVERWRITE. (At this point, you’ve probably guessed that I’m changing the DE names from what I use in my instance.)
SELECT
EmailAddress,
blnOptin,
dteUpdate,
strSource =
CASE
WHEN strSource is NULL THEN 'AcctPage'
ELSE strSource
END
FROM [MarketingEmailElection] mee
WHEREblnOptin = 1
AND
DATEDIFF(hour,mee.dteUpdate,GETDATE())<72
Query - now, get only the “yes” requests that are newer than the “no” (unsubscribe) requests in SuppressionMaster. Target MeeYes2, OVERWRITE.
SELECT
mee1.EmailAddress,
mee1.blnOptin,
mee1.dteUpdate,
sm.dteModified,
mee1.strSource
FROM
[MeeYes1] mee1
INNER JOIN
[SuppressionMaster] sm
ON mee1.EmailAddress = sm.EmailAddress
WHERE
mee1.dteUpdate > sm.dteModified
AND
mee1.blnOptin = 1
UNION
SELECT
mee1.EmailAddress,
mee1.blnOptin,
mee1.dteUpdate,
NULL as dteModified,
mee1.strSource
FROM
[MeeYes1] mee1
WHERE
NOT EXISTS (select 1 from [SuppressionMaster] sm
where sm.EmailAddress = mee1.EmailAddress )
Query - now get all the records that are in SuppressionMaster unless they’re in MeeYes1. Target SuppressMasterTransit, OVERWRITE. We need to do this because we need to remove the new “yes” entries from SuppressionMaster.
SELECT
mee1.EmailAddress,
mee1.blnOptin,
mee1.dteUpdate,
sm.dteModified,
mee1.strSource
FROM
[MeeYes1] mee1
INNER JOIN
[SuppressionMaster] sm
ON mee1.EmailAddress = sm.EmailAddress
WHERE
mee1.dteUpdate > sm.dteModified
AND
mee1.blnOptin = 1
UNION
SELECT
mee1.EmailAddress,
mee1.blnOptin,
mee1.dteUpdate,
NULL as dteModified,
mee1.strSource
FROM
[MeeYes1] mee1
WHERE
NOT EXISTS (select 1 from [SuppressionMaster] sm
where sm.EmailAddress = mee1.EmailAddress )
Query - now we write them back to SuppressMaster, OVERWRITE. At some point, I’d love to replace this with an SSJS SQL “DELETE” query instead.
SELECT
EmailAddress,
dteOptOut,
dteModified
FROM
[SuppressionMasterTransit]
Query - now we need to make a list of the records that need to be updated in _Subscribers. Target SubscribersUpdate, OVERWRITE
SELECT
y2.EmailAddress as [Email Address],
y2.EmailAddress as [Subscriber Key],
'active' as status,
y2.blnOptin,
y2.dteUpdate,
s.DateUnsubscribed
FROM
[MeeYes2] y2
INNER JOIN
[_Subscribers] s
ON s.EmailAddress = y2.EmailAddress
Now that we’ve got that file, we need to Export it to our SFMC SFTP Storehouse.
And then transfer it from the Storehouse to the SFTP Import Directory.
And then import it into _Subscribers, update.
Get everyone from Yes1 in the past 24 hours and place into Yes3, OVERWRITE.
SELECT
yes1.EmailAddress,
yes1.dteUpdate,
yes1.strSource
FROM
[MeeYes1] yes1
WHERE
yes1.dteUpdate > dateadd("d",-1,GETDATE())
AND
yes1.blnOptin = 1
Place everyone from Yes3 into the “optin” data extension. A different process will read from this Data Extension and send the latest blog email to all new opt-ins. It’s designed to only send them the email once, even if they appear in here more than once.
SELECT
yes1.EmailAddress,
yes1.dteUpdate,
yes1.strSource
FROM
[marketing_email_election_yes1_prod] yes1
WHERE
yes1.dteUpdate > dateadd("d",-1,GETDATE())
AND
yes1.blnOptin = 1
SuppressionMaster and SuppressionMasterTransit