Send Transactional Email via API in Salesforce Marketing Cloud
Transactional emails are not your typical marketing blasts. They are personalized messages sent in response to specific user actions, such as confirming a purchase or resetting a password. Unlike promotional emails, which aim to drive sales, transactional emails focus on delivering essential information. This distinction is crucial for businesses aiming to maintain a clear line of communication with their customers.
The Power of APIs in Sending Transactional Emails
Salesforce Marketing Cloud offers a robust REST API designed for sending transactional emails. This API simplifies the process for developers, allowing them to send emails to individual recipients with ease. Here’s a basic example of how to send a transactional email using the API:
Send single message to recipient
To send a single transactional message to a recipient, we need to implement the provided API.
POST https://{subdomain}.rest.marketingcloudapis.com/messaging/v1/email/messages/{messageKey}
{
"definitionKey": "DEFINITION_KEY",
"recipient": {
"contactKey": "CONTACTKEY",
"to": "recipient@example.com",
"attributes": {
"RequestAttribute_1": "value_1",
"RequestAttribute_2": "value_2"
}
}
}
The definition key is taken from your transactional journey API Event.

If you don’t want to use transactional journeys, you will need to use the API to create definition keys manually.POST https://{subdomain}.rest.marketingcloudapis.com/messaging/v1/email/definitions
Implement the API call in your CloudPage lead-capture form
When building a lead-capture flow on a CloudPage, you often need more than just storing data in a Data Extension. In many cases, you also want to trigger a transactional message. The good news is that when you implement a transactional API call, saving the data into a Data Extension becomes a byproduct – as long as the Data Extension is selected in the transactional journey.
<script runat=server>
Platform.Load("core","1.1.1");
//amended helper functions from https://www.ssjsdocs.xyz/email-studio/triggeredsends/send.html
var getToken = function(setup) {
var config = {
url : setup.authBaseURI + "v2/token",
contentType : "application/json",
payload : {
"client_id": setup.clientId,
"client_secret": setup.clientSecret,
"grant_type": "client_credentials"
}
}
var req = HTTP.Post(config.url, config.contentType, Stringify(config.payload));
if (req.StatusCode == 200) {
var res = Platform.Function.ParseJSON(req.Response[0]);
return res.access_token;
} else {
return false;
}
},
triggerEvent = function(token, setup, data) {
var config = {
url : setup.restBaseURI + "messaging/v1/email/messages/" + setup.guid,
contentType : "application/json",
headerName : ["Authorization"],
headerValue : ["Bearer " + token],
payload : {
definitionKey: setup.eventDefinitionKey,
recipient: data
}
}
var req = HTTP.Post(config.url, config.contentType, Stringify(config.payload), config.headerName, config.headerValue);
if (req.StatusCode == 202) {
var res = Platform.Function.ParseJSON(req["Response"][0]);
if (res && res.errorcode == 0) return true;
} else {
return false;
}
},
sha1 = function(str) {
var script = "%%[ SET @result = SHA1('" + str + "') ]%%%%=v(@result)=%%";
return Platform.Function.TreatAsContent(script);
},
getFingerprint = function () {
function safe(v) {
try { return (v || v === 0) ? v : ""; } catch(e){ return ""; }
}
collectServerAttrs = () {
var a = {};
try {
//a.IsSSL = safe(Platform.Request.IsSSL);
//a.Method = safe(Platform.Request.Method);
//a.Browser = safe(Platform.Request.Browser); // showing always different version number
//a.Version = safe(Platform.Request.Version); // empty
//a.MajorVersion = safe(Platform.Request.MajorVersion); // empty
//a.MinorVersion = safe(Platform.Request.MinorVersion); // empty
a.UserAgent = safe(Platform.Request.UserAgent);
} catch(e) {
Write("<!-- Error collecting request data: " + e.message + " -->");
}
return a;
}
function getSortedKeys(obj) {
var keys = [];
for (var k in obj) { if (obj.hasOwnProperty(k)) keys[keys.length] = k; }
var swapped = true;
while (swapped) {
swapped = false;
for (var i=0; i<keys.length-1; i++) {
if (keys[i] > keys[i+1]) {
var tmp = keys[i];
keys[i] = keys[i+1];
keys[i+1] = tmp;
swapped = true;
}
}
}
return keys;
}
function computeFingerprint(attrs) {
try {
var keys = getSortedKeys(attrs);
var raw = "";
for (var i=0; i<keys.length; i++) {
raw += keys[i] + "=" + attrs[keys[i]];
if (i < keys.length - 1) raw += "|";
}
var digest = sha1(raw);
return { id: digest, serialized: raw, attrs: attrs };
} catch(e) {
Write("<!-- Hash error: " + e.message + " -->");
return { id: "error", serialized: "", attrs: attrs };
}
}
try {
var attrs = collectServerAttrs();
return computeFingerprint(attrs);
} catch(e) {
Write("<!-- Fingerprint error: " + e.message + " -->");
return { id: "error", serialized: "", attrs: {} };
}
},
getHoursLessEST = function(hoursLess) {
var hoursLess = hoursLess || 1;
var jsDate = new Date();
jsDate.setHours(jsDate.getHours() - hoursLess);
Variable.SetValue("@date", jsDate);
var result = Platform.Function.TreatAsContent(
"%%=FormatDate(@date, 'yyyy-MM-dd','hh:mm:ss')=%%"
);
return result;
}
//capture browser information and fingerprint
Variable.SetValue('@ip', Platform.Request.ClientIP);
Variable.SetValue('@request', Platform.Request.Method);
Variable.SetValue('@source', Platform.Request.RequestURL);
Variable.SetValue('@fingerprint', sha1(getFingerprint().id + Platform.Request.ClientIP))
</script>%%[IF @request=="POST" THEN
/* user form data */
SET @thankyouPage = 1111 /* Cloud page ID to redirect after submission */
SET @email = Lowercase(RequestParameter("email"))
SET @firstname = RequestParameter("firstname")
SET @lastname = RequestParameter("lastname")
SET @token = RequestParameter("token")
SET @emailMd5 = Uppercase(MD5(@email,"UTF-16"))
/* default values */
SET @error = false
SET @rows = LookupRows("LEAD_FORM_LOGGING", "token", @token)
IF NOT EMPTY(@token) AND RowCount(@rows) == 1 THEN
SET @row = Row(@rows, 1)
SET @dateCreated = Field(@row, "date")
SET @dateSubmitted = Field(@row, "date_submitted")
SET @guid = Field(@row, "guid")
IF NOT EMPTY(@dateCreated) AND EMPTY(@dateSubmitted) THEN
SET @now = Now()
/* Update record */
UpdateData(
"LEAD_FORM_LOGGING",
1,
"token", @token,
"date_submitted",@now,
"email", @email,
"name", @firstname,
"lastname", @lastname,
"segment", @segment
)
]%%
<script runat="server">
Platform.Load("Core","1");
// Pull AMPscript variables into SSJS
var dateCreatedStr = Variable.GetValue("@dateCreated");
var nowStr = Variable.GetValue("@now");
if (dateCreatedStr && nowStr) {
var dateCreated = new Date(dateCreatedStr);
var now = new Date(nowStr);
// Calculate time difference in seconds
var diffMs = now - dateCreated;
var diffSec = diffMs / 1000;
// Expose back to AMPscript if needed
Variable.SetValue("@diffSeconds", diffSec);
} else {
Variable.SetValue("@diffSeconds", -1);
}
// --- Settings ---
var deName = "LEAD_FORM_LOGGING",
fingerprint = Variable.GetValue("fingerprint"),
ipHash = Variable.GetValue("ipHash"),
source = Variable.GetValue("source"),
hoursLessEST = getHoursLessEST(1);
// --- DE reference ---
var de = DataExtension.Init(deName);
// --- Build SSJS Filter ---
var filter = {
LeftOperand: {
Property: "visitor_id",
SimpleOperator: "equals",
Value: fingerprint
},
LogicalOperator: "AND",
RightOperand: {
LeftOperand: {
Property: "date",
SimpleOperator: "greaterThan",
Value: hoursLessEST
},
LogicalOperator: "AND",
RightOperand: {
Property: "source",
SimpleOperator: "equals",
Value: source
}
}
};
var rows = de.Rows.Retrieve(filter);
Variable.SetValue("@submissionCount", rows ? rows.length : 0);
%%[
/* spam traps */
IF
EMPTY(RegExMatch(@FirstName, "(https?|www\.)", 0)) AND
EMPTY(RegExMatch(@LastName, "(https?|www\.)", 0)) AND
@submissionCount == 1 AND
@diffSeconds > 2
THEN
]%%<script runat="server" language="JavaScript">
Platform.Load("core", "1.1.1");
try {
var eventName = Variable.GetValue("@eventName");
if (!eventName) throw ''
var setup = {
authBaseURI: "https://xxx.auth.marketingcloudapis.com/",
restBaseURI: "https://xxx.rest.marketingcloudapis.com/",
clientId: '<client_id>',
clientSecret: '<client_secret>',
eventDefinitionKey: eventName// in case we have multiple newsletter services
}
if (Variable.GetValue("@mid"))
setup.mid = Variable.GetValue("@mid");
setup.guid = Variable.GetValue("@guid")
var source = Variable.GetValue("@source") ? Variable.GetValue("@source") : false;
var token = getToken(setup);
var attributes = {
"FirstName": Variable.GetValue("@firstname"),
"LastName": Variable.GetValue("@lastname"),
"EmailAddress": Variable.GetValue("@email"),
"Segment": Variable.GetValue("@segment"),
"Locale": Variable.GetValue("@locale"),
"Guid": Variable.GetValue("@guid"),
"AlreadyRegistered": Variable.GetValue("@alreadyRegistered")
}
var additionalFields = Variable.GetValue("@additionalFields");
if (source)
attributes.source = source;
if (!isNullOrEmpty(additionalFields)){
var i = 0,
additionalFieldsArray = additionalFields.split(",");
/* Get additional fields from FORM DATA */
for (i;i<additionalFieldsArray.length;i++)
attributes[additionalFieldsArray[i]] = Platform.Request.GetFormField(additionalFieldsArray[i]);
//throw Platform.Function.Stringify(attributes);
}
var recipient = {
"contactKey": Variable.GetValue("@emailMd5"),
"to": Variable.GetValue("@email"),
"attributes": attributes
}
if (token) success = triggerEvent(token, setup, recipient)
if (success) Platform.Function.UpdateData("LEAD_FORM_LOGGING",["guid"], [Variable.GetValue("@guid")],["email_sent"],["True"])
} catch (e) {
// log error
}
</script>%%[
/*after email sent*/
ENDIF
ENDIF
ENDIF
/* after form submitted redirect to confirm email page*/
Redirect(CloudPagesURL(@thankyouPage, "email", @email, "locale",concat(@country, "-", @language),'settings',@settings))
ELSE
SET @ipHash = SHA1(@ip)
SET @guid = GUID()
SET @token = SHA1(@guid)
SET @insertStatus = InsertDE(
"LEAD_FORM_LOGGING",
"visitor_id", @fingerprint,
"ip_hash", @ipHash,
"guid", @guid,
"source", @source,
"token", @token
)
ENDIF
]%%
<form>
<input type="text" name="firstname">
<input type="text" name="lastname">
<!-- insert token to hidden field -->
<input type="hidden" name="token" value="%%=v(@token)=%%">
<input type="submit">
</form>
Now that we have our helper functions in place we can continue with our transactional API implementation.
Process form with AMPscript
%%[IF @request=="POST" THEN
/* global unique id for link generation */
SET @guid = GUID()
/* user form data */
SET @email = Lowercase(RequestParameter("email"))
SET @firstname = RequestParameter("firstname")
SET @lastname = RequestParameter("lastname")
/* assign contact key */
SET @emailMd5 = Uppercase(MD5(@email,"UTF-16"))
]%%
Process form with SSJS
<script runat="server">
Platform.Load("Core", "1");
// only run when POST
if (Request.Method == "POST") {
// global unique id for link generation
var guid = Platform.Function.GUID();
// user form data
var email = Request.GetFormField("email");
email = email ? email.toLowerCase() : "";
var firstname = Request.GetFormField("firstname");
var lastname = Request.GetFormField("lastname");
// assign contact key (MD5 UTF-16)
var emailMd5 = Platform.Function.MD5(email, "UTF-16").toUpperCase();
// you can now use guid, email, firstname, lastname, emailMd5
}
</script>
Send transactional email in SSJS
<script runat="server" language="JavaScript">
Platform.Load("core", "1.1.1");
var eventName = '<event_definition_key>';// in case this is dynamically populated add you logic here
try {
var setup = {
authBaseURI: "https://xxx.auth.marketingcloudapis.com/",
restBaseURI: "https://xxx.rest.marketingcloudapis.com/",
clientId: <client_id>
clientSecret: <client_secret>,
eventDefinitionKey: eventName// in case we have multiple newsletter services
}
var token = getToken(setup);
var attributes = {
"FirstName": Variable.GetValue("@firstname"), //or firstname when SSJS is used to process form
"LastName": Variable.GetValue("@lastname"), //or lastname when SSJS is used to process form
"EmailAddress": Variable.GetValue("@email"), //or email when SSJS is used to process form
"Guid": Variable.GetValue("@guid"), //or guid when SSJS is used to process form
}
var recipient = {
"contactKey": Variable.GetValue("@emailMd5"), //or guid when SSJS is used to process form
"to": Variable.GetValue("@email"),//or email when SSJS is used to process form
"attributes": attributes
}
var success = false;
if (token) success = triggerEvent(token, setup, recipient)
if (!success) {
// handle emails that were not sent here
}
} catch (e) {
//handle errors here
}
</script>
Personalization and Dynamic Content
One of the standout features of using the Salesforce API for transactional emails is the ability to incorporate personalization and dynamic content. By tailoring messages to individual recipients, businesses can enhance engagement and improve customer satisfaction. For example, including the recipient’s name or specific order details can make the email feel more relevant and personal. You can include any fields for personalization in the attributes payload, and these fields then become automatically available in the email template – just as they are when using a Data Extension as the source for your marketing campaigns.
Integrating Double Opt-In Processes
To further enhance user engagement, businesses can implement a double opt-in process alongside their transactional emails. This ensures that users have explicitly consented to receive communications, fostering trust and compliance with regulations. By integrating Service Cloud with Marketing Cloud, companies can manage user consent effectively, ensuring that transactional emails are sent only to those who have opted in.
Practical Takeaways
- Utilize APIs: Leverage the Salesforce Marketing Cloud API to automate the sending of transactional emails.
- Focus on Personalization: Use dynamic content to tailor messages to individual recipients, enhancing engagement.
- Implement Double Opt-In: Ensure user consent through a double opt-in process, integrating with Service Cloud for better management.
- Implement spam protection: Make sure you add spam protection to your forms as soon as possible. Without it, double opt-in forms can be abused to send malicious content to users.
Implement spam protection
To add a simple layer of protection to your CloudPage forms bef
đź”’ This content is for Premium Subsribers only.
Please log in to preview content. Log in or Register
You must log in and have a Premium Subscriber account to preview the content.
When upgrading, please use the same email address as your WordPress account so we can correctly link your Premium membership.
Please allow us a little time to process and upgrade your account after the purchase. If you need faster access or encounter any issues, feel free to contact us at info@martechnotes.com or through any other available channel.
To join the Discord community, please also provide your Discord username after subscribing, or reach out to us directly for access.
You can subscribe even before creating a WordPress account — your subscription will be linked to the email address used during checkout.
Premium Subscriber
19,99 € / Year
- Free e-book with all revisions - 101 Adobe Campaign Classic (SFMC 101 in progress)
- All Premium Subscriber Benefits - Exclusive blog content, weekly insights, and Discord community access
- Lock in Your Price for a Full Year - Avoid future price increases
- Limited Seats at This Price - Lock in early before it goes up





