SFMC Contact Deletion
Marketing Automation is as powerful as the database quality behind it. To make it good you must first remove the bad. Let's do it.
Outline
- Learn everything about SFMC Contact Deletion Process
- Check out sample segments just waiting to be deleted
- Create privacy-focused Contact Deletion Archive
Contact Deletion in SFMC
Salesforce Marketing Cloud makes Contact Deletion not only easy but also powerful. The out-of-the-box solution does most of the heavy lifting, leaving us the decision of who and when should be deleted from our platform.
But before we start the cleanup, let's see why we should do it in the first place. It's not easy to get new opted-in Contacts, so we shouldn't take removal lightly. However, there are multiple good reasons to do that:
- Compliance: Many legal systems (like European GDPR) allow subscribers to request contact deletion.
- Licensing: A marketing Cloud licence allows you to store a limited number of Contacts.
- Performance: The more Contacts you have, the slower will be your Journeys and Automations.
- Cost: Sending to dead Contacts means burning SuperMessages without any value in return.
- Deliverability: Sending to dead or bad quality Contacts hurts your email deliverability and inbox placement.
- Analytics: Bad quality data negatively impacts the quality of your analytics, making it harder to make the right decisions.
Once we know why we should delete, it's time to see what exactly is the built-in SFMC Contact Deletion process doing behind the scenes:
- It manages supression period for contacts requested for deletion:
- Suppression in Contact Builder hides the contacts and blocks the possibility to create a new one with the same Contact Key.
- Suppression in Email Studio hides the contacts and blocks them from receiving Emails or interacting with Triggered Sends.
- Suppression in Journey Builder blocks the contacts from entering Journeys and Automations.
- Suppression in Mobile Studio (MobileConnect, MobilePush, GroupConnect) hides the contacts and blocks them from receiving any communication and doing any interactions (exception - block requests are still processed).
- Suppression in Einstein hides the contacts and blocks both Recommendations and Analytics.
- After the suppression period, it deletes contacts from:
- Sendable Data Extensions
- Populations
- Lists
- Groups
- Journeys
- Automations
- Einstein
As you can see, a lot out-of-the-box. But what is the purpose of the suppression period when we just want to delete some contacts?
Contact Deletion is irreversible. Once we remove the contact, it is deleted for good, and there is no option to bring it back. Sure, you can add it again, but it won't have any past data. So, if you are not confident in your Contact Deletion process, you might configure suppression period to have some time for reflection. That's why it is there by default (currently at two days). If you find a mistake during that period, put a Support ticket as soon as possible with high priority.
There is also a second reason for the suppression period. Depending on the parallel processes run in your Marketing Cloud, the number of contacts requested for deletion, as well as the volume of data in the system, SFMC Contact Deletion can be a very time-consuming process. Suppression makes sure that even if it takes longer than you would like, the Contact will not get any unwanted communication.
Few places are not targeted by the out-of-the-box Marketing Cloud Contact Deletion process:
- Non-Sendable Data Extensions » Can be removed with either retention or heavy custom scripting
- Synchronized Data Extensions » Can be removed in the source system
- Triggered Send Lists » Can be removed using API method
It also ignores any data that is not connected to the deleted contact by Contact Key / Subscriber Key / Contact ID, so be sure to have a proper deduplication process in place for compliance.
Enable Contact Deletion
To use the Contact Deletion Process in Salesforce Marketing Cloud, you must enable it first. To do this, you need to have a Marketing Cloud Administrator role.
- Go to parent Business Unit.
- Go to Audience Builder » Contact Builder.
- Click Contacts Configuration in the top menu.
- Enable the process in the Contact Delete section.
- Configure suppression period by clicking Manage Settings (default is two days).
- Save.
If you want the Contact Deletion process to trigger automatically after starting it, set the suppression period to 0 days. It will be added to the processing queue instantly after the request.
For the most popular SFMC tenant type - Enterprise 2.0 - the deletion must happen at the parent level and automatically apply across all Business Units.
If you have a different tenant type, check the official documentation.
Manual Contact Deletion
Once the Contact Deletion is enabled and configured, you can start cleaning. The easiest way is to leverage manual deletion options available in the Contact Builder.
Manual Contact Deletion in Email Studio will remove the contact only from Email Studio. To trigger the Contact Deletion Process, you must leverage Contact Builder or API method.
Delete contacts from All Contacts
This method is best if you have just a few contacts you want to delete. It's simple, it's fast, it's constrained.
- Go to parent Business Unit.
- Go to Audience Builder » Contact Builder.
- Click All Contacts in the top menu.
- Find a contact(s) you want to delete and click checkboxes next to them.
- Click the trash icon and select Delete selected contacts.
Using this method, you can delete multiple contacts, but only from a single page of results. If you navigate to the next page of results, previously checked contacts will no longer be part of your selection.
If you want to remove more contacts, leverage the Data Extension method shared below.
Delete all contacts from List or Data Extension
This method will be better if you want to remove a significant number of contacts once but don't want the hassle of the most potent API method.
- Go to parent Business Unit.
- Go to Audience Builder » Contact Builder.
- Click All Contacts in the top menu.
- Without selecting any contact, click the trash icon.
- Click Delete contacts from list / data extension depending on your needs.
- Select the Sendable Data Extension used for the Contact Deletion process.
- Decide whether the Data Extension itself should stay in the system after SFMC deletes the contacts.
- Click Delete Contacts.
- Review and confirm by clicking Delete.
This method allows you to delete up to one million contacts per operation. If you want to remove more than that, you will have to run the above steps multiple times or leverage contact deletion automation.
SFMC API Contact Deletion
Previous methods are nice, but they require manual triggering in the Salesforce Marketing cloud each time you run the Contact Deletion Process. There are, however, many use cases where you might want to trigger it automatically - for example, from a different system responsible for managing privacy.
Thankfully, SFMC offers a solution for that as well with REST API. To implement it:
- Create authentication endpoint with correct permissions with Installed Package
- Go to Setup in Parent Business Unit.
- Within the Platform Tools section, roll out Apps and click Installed Packages.
- Click New in the top right.
- Provide a meaningful name and description for your Installed Package.
- Add Component - API Integration.
- Select the Server-to-Server integration type.
- Select Read and Write for List and Subscribers within the Contacts section. Write is needed for Deletion. Read - for checking the deletion status.
- Click Save.
- Copy Client Id, Client Secret and Client Base (the unique string in either of the API URIs that is between
https://
and API type). - Leverage the contact deletion endpoint in the solution of your choice:
https://CLIENT_BASE.rest.marketingcloudapis.com/contacts/v1/contacts/actions/delete
.
This single endpoint will allow you to trigger the Contact Deletion process in three ways by adding a query string at the end. All scenarios will use the same Method, Content-Type and Authorization, but the endpoint and payload will be different:
- Method: POST
- Content-Type: application/json
- Authorization: Bearer TOKEN
Contact Deletion by Contact ID
Endpoint: https://CLIENT_BASE.rest.marketingcloudapis.com/contacts/v1/contacts/actions/delete?type=ids
{
"values": [1111111, 22222222],
"deleteOperationType": "ContactAndAttributes"
}
This endpoint's values
is an array of Contact IDs (integers).
Contact Deletion by Contact Key
Endpoint: https://CLIENT_BASE.rest.marketingcloudapis.com/contacts/v1/contacts/actions/delete?type=keys
{
"values": ["A1B2C3D4", "E5F6G7H8"],
"deleteOperationType": "ContactAndAttributes"
}
This endpoint's values
is an array of Contact Keys (strings).
Contact Deletion using Data Extension
Endpoint: https://CLIENT_BASE.rest.marketingcloudapis.com/contacts/v1/contacts/actions/delete?type=listReference
{
"deleteOperationType": "ContactAndAttributes",
"targetList": {
"listType": { "listTypeID": 3 },
"listKey": "contactDeletionDEKey"
},
"deleteListWhenCompleted": false,
"deleteListContentsWhenCompleted": true
}
In this endpoint, you have control over three values:
listKey
: the external key of the Data Extension you use for the Contact Deletion process.deleteListWhenCompleted
: whentrue
, it will delete the Data Extension during the Contact Deletion process.deleteListContentsWhenCompleted
: whentrue
it will delete records from the Data Extensions during Contact Deletion process.
Contact Deletion with Automation Studio
With the API Contact Deletion we are not limited to external triggers. We can also leverage AMPScript or SSJS to trigger it directly from Marketing Cloud.
The most popular use case is a Contact Deletion Automation scheduled to clean up the database regularly. It's a great idea to keep your SFMC instance clean without much of a hassle.
My recommended approach is to create a dedicated Pending Contact Deletion Shared Data Extension that will store records to be deleted along with some context and remove its content nightly with the Script Activity.
Contact Deletion Pending Data Extension
If you want to capture contacts-to-be-deleted from multiple Business Units, make it Shared Data Extension.
Name | DataType | Default Value | Length | Nullable |
---|---|---|---|---|
🔑 SubscriberKey | Text | 18 | No | |
DeletionReason | Text | 50 | Yes | |
DeletionDate | Date | Current date | Yes |
External Key: contact-deletion-pending-de
Sendable: true
You don't need anything but the Subscriber Key for essential contact deletion. However, I prefer to add a bit more context that I can leverage in the Contact Deletion archive described later in this article.
Contact Deletion Script Activity
Below you can find ready-to-use SSJS snippet for Script Activity. Change clientSecret
, clientID
and clientBase
variables to data you saved during Installed Package configuration. If needed, change also the contactDeletionDEKey
, errorDE
and automationName
variables to reflect your configuration.
To learn more about errorDE
check my debugging guide. You will find there also details about helper functions from the script.
<script runat="server">
Platform.Load('core', '1')
/* -------------------------------------------------------------------------
1. Global Variables
1.1. Contact Deletion DE Key
1.2. REST API Authorization
1.3. Error Handling
2. Helper Functions
2.1. Debugging
2.2. Error handling
3. Main Script
3.1. REST API Authorization
3.2. Contact Deletion Process
-------------------------------------------------------------------------- */
/* ----------------------------------------------------------------------- */
/* ------------------------- 1. GLOBAL VARIABLES ------------------------- */
/* ----------------------------------------------------------------------- */
/* ------------------------ 1.1 Contact Deletion DE Key -------------------------- */
var contactDeletionDEKey = 'contact-deletion-pending-de';
/* ------------------------ 1.2 REST API Authorization --------------------------- */
var payload, endpoint, response;
var clientSecret = 'clientSecretFromInstalledPackage';
var clientID = 'clientIDFromInstalledPackage';
var clientBase = 'clientBaseUrl'
var contentType = 'application/json';
var debugging = false;
/* ------------------------ 1.3 Error Handling --------------------------- */
var errorDE = 'error-log-de';
var automationName = 'contact-deletion-process';
/* ----------------------------------------------------------------------- */
/* ------------------------- 2. HELPER FUNCTIONS ------------------------- */
/* ----------------------------------------------------------------------- */
/* --------------------------- 2.1. Debugging ---------------------------- */
/**
* @function debugValue
* @description Outputs provided description and SSJS value to front-end in a type-safe & consistent way
* @param {string} description - Describes meaning of the second parameter in the output
* @param {*} value - The value that needs to be debugged
*/
function debugValue(description, value) {
Write(description + ': ' + (typeof value == 'object' ? Stringify(value) : value) + '<br><br>');
};
/* -------------------------- 2.2. Error handling ------------------------ */
/**
* @function handleError
* @description Adds the error with context to error logging Data Extension and redirects to an error page.
* @param {Object} error - The caught error object. It can come from the try/catch block or be manually created.
* @param {string} error.message - First error key stores a short error message describing the issue.
* @param {string} error.description - Second error key stores detailed error path helping with root cause analysis
*/
function handleError(error) {
if (debugging) {
debugValue('Found error', error);
} else {
Platform.Function.InsertData(errorDE, ['ID', 'ErrorSource', 'ErrorMessage', 'ErrorDescription'], [GUID(), automationName, error.message, error.description]);
};
};
/* ----------------------------------------------------------------------- */
/* --------------------------- 3. MAIN SCRIPT ---------------------------- */
/* ----------------------------------------------------------------------- */
/* ------------------- 3.1. REST API Authorization --------------------- */
endpoint = 'https://' + clientBase + '.auth.marketingcloudapis.com/v2/token';
payload = {
client_id: clientID,
client_secret: clientSecret,
grant_type: 'client_credentials'
};
if (debugging) debugValue('Payload', payload);
try {
response = HTTP.Post(endpoint, contentType, Stringify(payload));
} catch (error) {
handleError(error);
}
var accessToken = Platform.Function.ParseJSON(response['Response'][0]).access_token;
if (debugging) debugValue('AccessToken', accessToken);
/* ------------------- 3.2. Contact Deletion Process --------------------- */
endpoint = 'https://' + clientBase + '.rest.marketingcloudapis.com/contacts/v1/contacts/actions/delete?type=listReference';
payload = {
deleteOperationType: 'ContactAndAttributes',
targetList: {
listType: { listTypeID: 3 },
listKey: contactDeletionDEKey
},
deleteListWhenCompleted: false,
deleteListContentsWhenCompleted: true
};
var headerNames = ['Authorization'];
var headerValues = ['Bearer ' + accessToken];
try {
response = HTTP.Post(endpoint, contentType, Stringify(payload), headerNames, headerValues);
if (debugging) debugValue('Response', response);
} catch (error) {
handleError(error);
}
</script>
Monitor Contact Deletion Process
There are three ways to monitor what is happening with your Contact Deletion requests (you should perform all three from the parent Business Unit):
- In Contact Builder, in the top menu, click All Contacts. Click the trash icon in the top right, and select View Pending Deletions.
- In Contact Builder, in the top menu, click Contact Analytics. You will see a full Contact Deletion Dashboard.
- Request details using Contact Deletion Details API.
Which contacts should you delete?
You should always align this question with the specific business needs of your company. However, there are some very popular segments that may inspire you.
You can add them within the same Automation as the Contact Deletion Script Activity to ensure they are captured right before cleanup. You may also create separate Automations on your child Business Unit to capture BU-specific candidates for cleanup using data available only there.
Remember to test those recommendations and queries (for example, using Query Studio) before implementing them in your Contact Deletion Process, as even a small mistake might lead to irreversible data loss.
Contacts deleted in Salesforce CRM
If you use Marketing Cloud Connect to integrate your Salesforce CRM with the Marketing Cloud, you should think about synchronizing deletions between those two systems. It does not happen automatically. By default, contacts deleted in SF CRM are removed from Synchronized Data Extensions and stay in other Marketing Cloud tables.
You can, however, add this using Query Activity in Automation Studio. For the email channel, it's straightforward:
SELECT
subscriber.SubscriberKey
, 'Deleted in SF CRM' AS DeletionReason
FROM _Subscribers AS subscriber
LEFT JOIN Contact_Salesforce AS contact
ON contact.Id = subscriber.SubscriberKey
WHERE contact.Id IS NULL
If you are also using other channels, you can include their System Data Views to the query.
Leads and Users deleted or filtered in Salesforce CRM
This method is also great for removing deleted or unused records from other objects counted against your All Contacts licence limits - Leads and Users. There are two caveats, however.
First, to remove unused Leads and Users, you must first stop them from synchronizing via Marketing Cloud Connect. Otherwise, they will come back to SFMC at the next scheduled synchronization. The best way to do it is to create a boolean field on those objects (for example, ShouldSyncToSFMC
) that would be true
only for records you need in your system. Depending on your use case, it might be neither (so just set false
as the default value), or it might be complicated (the calculated field will be your best friend). With such a boolean, you will be able to select it as a filter for your Marketing Cloud Connect object synchronization.
Second is the problem of finding those records in the Marketing Cloud. The easy way is to create a copy of Synchronized Data Extensions before filtering them. However, this will not always be possible, and it won't capture Leads and Users that were removed in the past from CRM.
If you have Mobile Studio in your SFMC licence, you have a nice workaround:
- Go to Contact Builder.
- Click All Contacts in the top menu.
- Select the Mobile List tab.
- Click Create Mobile List button in the top right.
- Leave MobileConnect as the channel and select Filtered List as a type. Click Ok.
- Select All Contacts and click Select.
- Remove prepopulated Mobile Number filter and on the left pane select System Data » Contact.
- Drag and drop Contact Key and configure it to
is not null
. Click Done. - Click the Save as Filter button in the top right.
- Provide a sensible name (like "AllContactsList") and click Save.
All Contacts view shows many cool data fields, but unfortunately, most are no longer available after the export to the List.
Mobile List fields are: SubscriberKey
, CustomerKey
, AudienceID
, TrackingCode
, AudienceCode
, SegmentCode
, SegmentName
, Priority
, SegmentID
, SplitID
.
So in nearly every case you will have to take the Subscriber Key and enrich it with JOIN
to other more interesting Data Extensions or System Data Views.
It will create and populate the list with all your Contacts. Save its name - it will be helpful for the Query.
If you want to remove all Leads and Users, you will need only this List:
SELECT SubscriberKey
FROM AllContactsList
WHERE
SubscriberKey LIKE '005%'
OR SubscriberKey LIKE '00Q%'
You might wonder what is happening within the WHERE
filter above - this is magic that is possible thanks to Salesforce ID structure. The ID is not entirely random. Instead, it has a specific format.
For our need here, the key is the three-character prefix that point to a specific Salesforce Object:
- 001 = Account
- 003 = Contact
- 005 = User
- 00Q = Lead
For the complete list, check Daniel Ballinger's cheat sheet.
If you want to remove only those Leads / Users that you no longer synchronize via filtered Marketing Cloud Connect, you will need to add a comparison similar to the Contacts use case.
SELECT contact.SubscriberKey
FROM AllContactsList AS contact
LEFT JOIN Leads_Salesforce AS lead
ON contact.SubscriberKey = lead.ID
LEFT JOIN Users_Salesforce AS user
ON contact.SubscriberKey = user.ID
WHERE
(
SubscriberKey LIKE '005%'
AND user.ID IS NULL
)
OR
(
SubscriberKey LIKE '00Q%'
AND lead.ID IS NULL
)
Contactless Contacts
Another group ripe for deletion are Contacts that you cannot contact. Multiple subgroups fill that definition:
- Suppressed contacts
- Hard bounced contacts
- Contacts that do not have any channel information (like an email address or mobile phone)
- Bad quality contacts (test records, fake emails, contacts that failed Email Studio List Detective)
- Hidden Triggered Send Managed List Subscribers
Before deletion, it might be worth saving some of this data for the future (especially suppressed contacts for compliance or hard bounced contacts for future deliverability).
Zombie Contacts
Apart from contactless contacts, there are always some zombies in each database. Contacts that you can contact - and you do - but they are not reacting in any way. No opens, no clicks, for months.
I know it might be hard to delete opted-in contacts when obtaining new ones is hard work, but you need to finish this toxic relationship after some time. Zombie contacts skew your analytics, put unnecessary strain on your instance, degrade your IP Reputation and create costs without providing any value to your business in return. You will be far better without them.
Align with your business the threshold for marking a contact as a zombie. It will be different from company to company, but my starting point recommendation is a contact that (1) Was sent at least six emails, (2) within at least three different months and (3) did not open or click any email.
- Makes sure that you tried enough times to make your opinion based on solid facts
- Makes sure that it's not a matter of out-of-office period, but a constant issue
- Makes sure that temporary problem is not leading to deletion of otherwise valuable contact
Before making the final decision, I recommend checking a few versions of those rules against your database to see which variation gives you the best results without being too lax.
Contact Deletion Archive
Deleting contacts is super fun, but sometimes you might need to know whom you have deleted. Or be able to debug recurring deletions to fix a leak in your data pipeline. Contact Deletion Archive is the way to go - it allows you to keep details of your deletions in a privacy-first way.
It is built with two additional elements on top of the standard Contact Deletion process described above.
Contact Deletion Archive Data Extension
It will store the information about past deletions allowing you to check multiple things securely:
- Subscriber Key of deleted contacts.
- Hashed Emails of the deleted contacts.
- Reason for the deletion.
- Date of first and last deletion (and, therefore, whether we deleted the SubscriberKey more than once).
Important: Make sure you configure it as NOT sendable Data Extension. Otherwise, your Contact Deletion Process will remove data also from it.
Name | DataType | Default Value | Length | Nullable |
---|---|---|---|---|
🔑 SubscriberKey | Text | 18 | No | |
HashedEmail | Text | 254 | Yes | |
DeletionReason | Text | 50 | Yes | |
FirstDeletionDate | Date | Current date | Yes | |
LastDeletionDate | Date | No |
Contact Deletion Archive Query Activity
We will need new Query Activity to fill that Archive Data Extension. One that moves the data from the Contact Deletion Pending Data Extension.
It will leverage the HASHBYTES
function to hash the Email Address. On the one hand, it will protect the privacy of the deleted contacts and make sure no one will use them for any send. On the other - it allows you to check the list of deleted contacts against known email by hashing it the same way.
SELECT
pending.SubscriberKey
, CONVERT(NVARCHAR(128), HASHBYTES('SHA2_256', sub.EmailAddress), 2) AS HashedEmail
, pending.DeletionReason
, pending.DeletionDate AS LastDeletionDate
FROM [Contact Deletion Pending DE] AS pending
INNER JOIN _Subscribers AS sub
ON sub.SubscriberKey = pending.SubscriberKey
This new Query Activity should be put in the Contact Deletion Automation between the Contact Deletion Query Activity that selects the contacts to-be-deleted and the Contact Deletion Script Activity that does the deed.