Developer Guide
The following guide and topics are current as of Sitecore Experience Platform 9.1. As updates are released, this guide will be updated as quickly as possible.
The EXM Developer Guide is a living guide intended to provide additional information that the Sitecore documentation site might not make available. However, it is not intended to be cut and paste of the Sitecore Documentation Site. Instead, this is a companion guide. Please provide comments below if any information is incomplete, incorrect, or not present.
Sitecore Documentation Site Links
- Sitecore 9.1 Email Experience Manager
- Sitecore 9.0 Email Experience Manager
- Email Experience Manager 3.5
- Email Experience Manager 3.4
Table of Contents
Roles of Email Experience Manager

The server roles of Email Experience Manager do not differ from the roles of Sitecore, with the exception of the Dedicated Dispatch Server. Otherwise, all of the roles do the same things, only EXM makes each role do additional work.
When using EXM, it’s important t note, that on ALL role servers EXM should be enabled via the Web.config as shown here.
<!-- EXM Enabled
Set this to anything other than 'yes' to disable EXM and its configuration.
-->
<add key="exmEnabled:define" value="yes"/>
Additionally, ALLservers should have the exact same keys defined in the ConnectionStrings.config as shown here:
<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
<add name="EXM.CryptographicKey" connectionString="0x3068480677720075338228396754938017850570187873738392546365112406" />
<add name="EXM.AuthenticationKey" connectionString="0x4110187119230947340592315047210744477554857314512310509679367459" />
</connectionStrings>
The keys listed above are for example only. You should generate and use
different keys.
Primary Content Management
This is the primary authoring environment. All content management and authoring should occur on this server. To define a Primary Content Management server, use the following role:define in the Web.config.
<!-- SUPPORTED SERVER ROLES
Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:
ContentDelivery
ContentManagement
Processing
Reporting
Standalone
DedicatedDispatch
Default value: Standalone
-->
<add key="role:define" value="ContentManagement"/>
Dedicated Dispatch Server
In practice, the Dedicated Dispatch Server is just a secondary content management server, with an additional responsibility: Dispatching email through EXM. To define a Sitecore instance as a Dedicated Dispatch Server:
<!-- SUPPORTED SERVER ROLES
Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:
ContentDelivery
ContentManagement
Processing
Reporting
Standalone
DedicatedDispatch
Default value: Standalone
-->
<add key="role:define" value="ContentManagement,DedicatedDispatch"/>
Content Delivery
The content delivery server is the primary web server that the public internet is served. To Email Experience Manager, the CD server be mapped to the public domain of your website, as configured in the Email Manager Root in EXM.
The Content Delivery role serves one purpose for EXM: It is the front door for open and click tracking requests, as well as subscription client access, redirects, and for displaying subscription and marketing preferences.
<!-- SUPPORTED SERVER ROLES
Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:
ContentDelivery
ContentManagement
Processing
Reporting
Standalone
DedicatedDispatch
Default value: Standalone
-->
<add key="role:define" value="ContentDelivery"/>
It’s also very important the every Content Delivery server has the same MachineKey information in the Web.config and that the EXM keys in the ConnectionString.config that are the same on all CM and Dispatch Servers.
Processing
This is the standard Sitecore Processing role. Additional configs are enabled for Email Experience Manager to assist in additional processing aggregation.
<!-- SUPPORTED SERVER ROLES
Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:
ContentDelivery
ContentManagement
Processing
Reporting
Standalone
DedicatedDispatch
Default value: Standalone
-->
<add key="role:define" value="Processing"/>
Reporting
This is the standard Sitecore Reporting role. Additional configs are enabled for Email Experience Manager to assist in additional reporting endpoints.
<!-- SUPPORTED SERVER ROLES
Specify the roles that you want this server to perform. A server can perform one or more roles. Enter the roles in a comma separated list. The supported roles are:
ContentDelivery
ContentManagement
Processing
Reporting
Standalone
DedicatedDispatch
Default value: Standalone
-->
<add key="role:define" value="Reporting"/>
EXM Pipelines
Email Experience Manager, as expected, follows all of the same design patterns that the rest of Sitecore uses when it comes to how EXM is connected to Sitecore. This means that for almost every process, there is a pipeline or a provider.
The following sections of this guide are intended for Certified Sitecore developers that already have an understanding of how Sitecore pipelines work. Aside from highlighting key pipelines, there will not be a full description of every processor, unless intentionally called out. Use of dotPeek, or your favorite decompiler, is advised.
Dispatch Pipelines
The most important pipeline is the <DispatchNewsletter> pipeline. When the Send button is clicked in EXM, or when an Automated Message is triggered, Sitecore EXM runs this pipeline on the Primary CM server. There is an abbreviated version of the <DispatchNewsletter> pipeline on the Dedicated Dispatch Servers too.
Sitecore provides a good page for detailing out these pipelines.
Primary CM
Sitecore.EmailExperience.ContentManagementPrimary.config
<DispatchNewsletter>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.CheckPreconditions, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.MoveToQueuing, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.DeployAnalytics, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.PublishDispatchItems, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.QueueMessage, Sitecore.EmailCampaign.Cm" resolve="true"/>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.MoveToProcessing, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.LaunchDedicatedServers, Sitecore.EmailCampaign.Cm" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.SendTestMessage, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.SendMessage, Sitecore.EmailCampaign.Cm" resolve="true">
<!-- The number of milliseconds to wait after dispatch is completed/aborted/paused in order to ensure logging statistics are updated across all dispatch servers. Can be set to 0 if there are no dedicated dispatch servers configured. -->
<Sleep>2000</Sleep>
</processor>
<!-- The WaitForDispatchToFinish pipeline processor should only be enabled if you have at least one dedicated dispatch server enabled.If you enable this processor you should disable the SendMessage processor. -->
<!--<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.WaitForDispatchToFinish, Sitecore.EmailCampaign.Cm" resolve="true">
<TimeToWaitBetweenChecks>1000</TimeToWaitBetweenChecks>
</processor>-->
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.MoveToSent, Sitecore.EmailCampaign.Cm" resolve="true"/>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.NotifyDispatchFinished, Sitecore.EmailCampaign.Cm" resolve="true"/>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.FinalizeDispatch, Sitecore.EmailCampaign.Cm" resolve="true" />
</DispatchNewsletter>
Dedicated Dispatch Server
Sitecore.EmailExperience.EmailProcessing.config
<DispatchNewsletter>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.CheckPreconditions, Sitecore.EmailCampaign.Cm" resolve="true"/>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.SendMessage, Sitecore.EmailCampaign.Cm" resolve="true" >
<!-- The number of milliseconds to wait after dispatch is completed/aborted/paused in order to ensure logging statistics are updated across all dispatch servers. Can be set to 0 if there are no dedicated dispatch servers configured. -->
<Sleep>2000</Sleep>
</processor>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.DispatchNewsletter.FinalizeDispatch, Sitecore.EmailCampaign.Cm" resolve="true" />
</DispatchNewsletter>
Be sure to adjust the <Sleep> value in a production environment.
Message Creation Pipelines
There are a couple of pipelines that deal with the individual creation/sending of an email to a single contact. For every contact being dispatched to, the following pipeline is executed on all DDS servers, as well as Primary CM assuming that dispatch hasn’t been disabled.
<!-- SEND EMAIL PIPELINE
This pipeline dispatches a single email through the SMTP server.
-->
<SendEmail>
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.FillEmail, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.SendEmail, Sitecore.EmailCampaign.Cm" resolve="true" />
<processor type="Sitecore.EmailCampaign.Cm.Pipelines.SendEmail.Sleep, Sitecore.EmailCampaign.Cm">
<!-- Number of milliseconds to put the thread to sleep for after an email has been sent. -->
<param desc="sleep">50</param>
</processor>
</SendEmail>
Adjust Sleep value as needed. For fast dispatching, change to 0.
If your SMTP service has a rate limit, adjust this to a higher value.
Deep in the FillEmail processor above, the <modifyHyperlink> pipeline is executed for every link in an email. This augments the link URL in the email to a RedirectURL link that provides the appropriate ciick tracking and analytics tracking needed. The links are also encrypted coming out of this pipeline.
<modifyHyperlink>
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.SkipAnchorLinks, Sitecore.EmailCampaign" />
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.SetServerUrl, Sitecore.EmailCampaign" resolve="true" />
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.SkipAlreadyProcessedHyperlink, Sitecore.EmailCampaign">
<RedirectPagePath>/sitecore%20modules/Web/EXM/RedirectUrlPage.aspx</RedirectPagePath>
</processor>
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.MapHostname, Sitecore.EmailCampaign" resolve="true" />
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.SetAnalyticsQueryStringParameters, Sitecore.EmailCampaign" />
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.HandleInternalLink, Sitecore.EmailCampaign" />
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.GeneratePreviewLink, Sitecore.EmailCampaign" />
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.GenerateHyperlink, Sitecore.EmailCampaign">
<RedirectPagePath>/sitecore%20modules/Web/EXM/RedirectUrlPage.aspx</RedirectPagePath>
<UrlQueryKey ref="settings/setting[@name='QueryStringKey.RedirectUrl']/@value" />
</processor>
<processor type="Sitecore.Modules.EmailCampaign.Core.Pipelines.GenerateLink.Hyperlink.EncryptQueryString, Sitecore.EmailCampaign">
<param desc="queryStringEncryption" ref="queryStringEncryption" />
</processor>
</modifyHyperlink>
Click Tracking Pipeline
On the Content Delivery servers, when a link is clicked on from an EXM message, the CD server processes the <redirectUrl> pipeline to process all of the appropriate trackings. This pipeline has a lot of useful processors. Once the processing is complete, the click action is redirected to the original URL intended.
<group groupName="exm.messageEvents">
<pipelines>
<!-- REDIRECT URL PIPELINE
This pipeline is executed when Email Experience Manager receives a request to redirect
a page request from an email link to the correct destination page.
-->
<redirectUrl>
<!-- Retrieves the message item associated with the redirect event. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.GetMessage, Sitecore.EmailCampaign.Cd" resolve="true"/>
<!-- Determines whether the link provided in the request is a reference to a page on the local web site. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CheckInternalLink, Sitecore.EmailCampaign.Cd" resolve="true" />
<!-- Constructs the URL to redirect the request to. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.SetRedirectToUrl, Sitecore.EmailCampaign.Cd" resolve="true">
<internalCarryoverFields hint="list:AddInternalCarryoverField">
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.MessageId']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.AnalyticsContactId']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.ContactIdentifierSource']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.ContactIdentifierIdentifier']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.Campaign']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.TargetLanguage']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.TestValueIndex']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
<carryoverField type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.CarryoverField, Sitecore.EmailCampaign.Cd">
<param desc="fieldKey" ref="settings/setting[@name='QueryStringKey.EmailHistoryEntryId']/@value" />
<param desc="urlPattern">SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</param>
</carryoverField>
</internalCarryoverFields>
</processor>
<!-- Registers the link click event in emailEventStorage and attaches the result to the pipeline argument. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.RegisterMessageEvent, Sitecore.EmailCampaign.Cd">
<param desc="eventStorage" ref="exm/emailEventStorage" />
<param desc="duplicateProtectionIntervalSecs"
ref="settings/setting[@name='EXM.DuplicateProtectionInterval']/@value" />
<param desc="logger" ref="exmLogger" />
</processor>
<!-- Registers custom page events. Internal page references matching the IgnoredUrlPattern will not add the event. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.RegisterPageEvents, Sitecore.EmailCampaign.Cd" resolve="true">
<IgnoredUrlPattern>SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</IgnoredUrlPattern>
</processor>
<!-- Triggers the campaign associated with the email message. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.TriggerCampaign, Sitecore.EmailCampaign.Cd" resolve="true">
<IgnoredUrlPattern>SubscriptionPreferences.ashx|.*ConfirmSubscription.aspx|.*Unsubscribe.aspx|UnsubscribeFromAll.aspx.*|.*sc_pd_view=1.*</IgnoredUrlPattern>
</processor>
<!-- Marks the current session as an email click session. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.MarkAsEmailClickSession, Sitecore.EmailCampaign.Cd" resolve="true"/>
<!-- Identifies the xDB contact related to the event in the xDB tracker. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.IdentifyContact, Sitecore.EmailCampaign.Cd" resolve="true">
<param desc="logger" ref="exmLogger" />
</processor>
<!-- Updates the classification of the identified contact if it is currently greater than a given threshold. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.UpdateContactClassification, Sitecore.EmailCampaign.Cd" resolve="true">
<LowerClassificationThreshold>900</LowerClassificationThreshold>
<NewClassification>0</NewClassification>
</processor>
<!-- Resets the email bounce counter of the identified contact to zero. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.ResetContactEmailBounceCount, Sitecore.EmailCampaign.Cd" resolve="true">
<EmailAddressesFacetName ref="model/entities/contact/facets/facet[@name='Emails']/@name" />
</processor>
<!-- Sets the channel id of the current visit according to the campaign activity associated with the email message. -->
<processor type="Sitecore.EmailCampaign.Cd.Pipelines.RedirectUrl.SetVisitChannelId, Sitecore.EmailCampaign.Cd" resolve="true"/>
</redirectUrl>
</pipelines>
</group>
EXM API’s
There are a number of API’s and .NET Service endpoints that can be referenced in solutions. The list below highlights some of the more common services.
EXM Client API Service
This service was covered in detail during the 25 Days of Sitecore EXM series. Day 21 describes this service in detail.
Subscription Manager Service
Available through Dependency Injection. This service can only be called and referenced on Content Management servers. This service does NOT work on CD’s.
using Sitecore.Data;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.Messages;
using Sitecore.XConnect;
using System;
namespace Sitecore.EmailCampaign.Cm
{
public interface ISubscriptionManager
{
bool Subscribe(ContactIdentifier contactIdentifier, Guid messageId, bool subscriptionConfirmation);
bool Unsubscribe(ContactIdentifier contactIdentifier, Guid messageId);
bool UnsubscribeFromAll(ContactIdentifier contactIdentifier, Guid managerRootId);
bool UnsubscribeFromAll(Contact contact, ManagerRoot managerRoot);
bool ConfirmSubscription(string cid);
string GetConfirmationKey(Guid recipientListId, ContactIdentifier contactIdentifier, ManagerRoot managerRoot);
bool ConfirmSubscription(ShortID id);
bool AddToGlobalOptOutList(ContactIdentifier contactIdentifier, ManagerRoot managerRoot);
bool RemoveContactFromList(ContactIdentifier contactIdentifier, Guid listId);
bool RemoveContactFromList(Contact contact, Guid listId);
bool AddContactToList(ContactIdentifier contactIdentifier, Guid listId);
Guid GetSubscriptionListFromMessage(MessageItem messageItem);
}
}
Manager Root Service
Available through Dependency Injection. This can also be run on all Sitecore role servers.
using Sitecore.Data.Items;
using System;
using System.Collections.Generic;
namespace Sitecore.Modules.EmailCampaign.Services
{
public interface IManagerRootService
{
List<ManagerRoot> GetManagerRoots();
ManagerRoot GetManagerRootFromId(Guid id);
ManagerRoot GetManagerRootFromItem(Item rootItem);
ManagerRoot GetManagerRootFromChildItem(Item childItem);
ManagerRoot GetManagerRoot(Guid managerRootId);
Guid CreateRoot();
}
}
Leave a Reply