Guide · updated October 2026
Pre-send verification in Salesforce Marketing Cloud: QA the test send, then gate the send on the approved file
The most expensive email incident is not a bug the QA missed. It is the version swap: QA approved the file on Tuesday, someone hotfixed a link in Content Builder on Wednesday, and the Thursday send went out unreviewed. Salesforce Marketing Cloud makes this easy to do and hard to notice, because the asset in Content Builder, the test send and the production send are three different things that look the same.
This guide covers both halves of pre-send verification in SFMC: what to check in the test send, because that is the only thing that matches what the recipient gets, and a gate that makes the send itself ask “is this exact file still the approved one?” before it fires.
Why SFMC sends go wrong after approval
- Content Builder is editable until the send. A hotfix to a link, a date, or a legal line after sign-off is one click, leaves no round behind it, and the approval still points at the old file.
- Links are rewritten at send time. Every href becomes a click-tracking address that redirects to the real page. The HTML you reviewed had the real URL; the recipient gets the wrapper, and the wrapper can strip parameters or point at a page from the previous send of the same asset.
- Dynamic content resolves per subscriber. A version block, an AMPscript lookup or a personalisation string shows one thing in the preview and another for the segment, and the French variant is rarely the one previewed.
- The platform adds its own parts. The unsubscribe and profile centre links, the view-as-webpage link, the physical address and the list-unsubscribe header come from the sender profile and the business unit, not from the HTML, and are wrong in a different place from the one the build was checked in.
- Images move to the content delivery domain. A picture that loaded from the agency’s server in the build loads from SFMC’s image host in the send, or does not.
What to check in the test send
Send a test to a seed inbox for every version and every language. Open it in Outlook on the web or Gmail, save it as an .eml (or view the message source and copy it), and check that file, not the Content Builder asset. SendLint reads the .eml directly, decodes the MIME parts and runs the full QA on what the recipient would see.
- Every link, followed through the click-tracking wrapper to the page it lands on, matches the deck’s links table; the campaign parameters survive the redirect; nothing points at staging or at last send’s page.
- The subject line and preheader are this version’s, in this language, and match the deck. A reminder sent with the launch subject line is the most common SFMC miss.
- The unsubscribe link and the profile centre link resolve to the client’s pages, in the right language. They are listed, never opened by a checker, and checked by hand from the seed inbox.
- The view-as-webpage link opens the same email, and the physical mailing address in the footer is the client’s current one.
- Dynamic content resolved correctly for each version: the right rate, the right product name, the right legal line, with no personalisation string left unresolved (
%%FirstName%%printed as text). - Images load from the content delivery domain, with the width and height attributes and alt text the build had.
- The copy, creative and accessibility checks still pass on the sent file: the platform’s rewriting should change links and nothing else.
The link half of this is what SendLint’s email link checker does on its own; the full email QA checklist covers the rest. The test send is round N of the campaign in SendLint: the same findings as the build, carried forward, plus anything the platform changed.
The gate, conceptually
Checking the test send proves the file was right when you looked. The gate proves it is still the same file when the automation runs. One request: hash the exact HTML you are about to send and ask SendLint whether that fingerprint is on a standing certificate.
Authorization: Bearer sl_live_…
verified: true · status: valid → proceed to send
verified: false · status: not_found → never approved, or edited after · stop
verified: false · status: superseded → a newer approval replaced this file · stop
verified: false · status: revoked → approval was withdrawn · stop
The hash is SHA-256 over the file’s exact UTF-8 bytes, hex encoded. Any byte changed after approval, even inside an invisible attribute, produces a different hash and a stop. The raw HTML never leaves your business unit; only its hash is sent, and the answer is scoped to your own workspace’s certificates.

The recipe in Automation Studio
- 1. Keep the approved file authoritative. The HTML in Content Builder should be byte-identical to the file QA approved in SendLint. Treat any edit inside SFMC as a new round: run it through SendLint and approve it, which mints a fresh certificate and marks the old one superseded.
- 2. Add a Script Activity before the Send Email activity. It runs server-side JavaScript. Read the API key from a secured Data Extension (never hard-code it), load the HTML you are about to send, hash it with
Platform.Function.SHA256(html, "UTF-8"), and call the gate withScript.Util.HttpRequestand the key in the Authorization header. - 3. Branch on the verdict. Parse the JSON. If
verifiedis true, set a variable the next step reads and let the automation continue. If not, throw: the throw stops the automation, and the returnedstatusandmessagesay exactly why in plain words. - 4. Fail closed. Wrap the whole activity in a try/catch that stops the send on any error: a network failure, a 401 on the key, or a malformed response must never let an unverified email through silently.
var req = new Script.Util.HttpRequest("https://sendlint.com/api/v1/verify?hash=" + hash);
req.method = "GET";
req.setHeader("Authorization", "Bearer " + apiKey);
var body = Platform.Function.ParseJSON(String(req.send().content));
if (body.verified === true) Platform.Variable.SetValue("@sendlintGate", "PROCEED");
else throw "SendLint gate blocked the send (" + body.status + "): " + body.message;
The complete Script Activity, with the key lookup, the line-ending retry and the fail-closed wrapper, is about forty lines; it ships with SendLint as pre-send-gate.ssjs together with a harness that proves the contract against the live API.
Hash the right bytes
The gate only works if the bytes you hash are the bytes SendLint approved, so be precise about which HTML that is:
- The template, not the merged send. Hash before personalisation is substituted and before SFMC rewrites the links for tracking. The approved file has
%%FirstName%%and the real URLs in it; so should the hashed one. - Line endings are not content. A browser upload converts line breaks to CRLF, so the certificate usually fingerprints the CRLF form; a file saved from an editor is often LF. The gate handles this: on
not_foundit hashes the other line-ending form and asks once more. Asupersededorrevokedanswer is final and never retried. - The simplest robust variant skips in-SFMC hashing. Compute the hash once at hand-off (
shasum -a 256 launch-en.htmlon any machine), store it in the Data Extension beside the asset, and let the Script Activity make the API call with the stored hash. Fewer moving parts, same guarantee, because the certificate is what is being checked, not the courier.
What the team sees when it stops
A blocked automation shows the thrown message in the activity’s error, and the Script Activity sets two variables the next step can read: the gate verdict and the reason. The reasons are the three statuses above, each with a sentence that says what to do: run the edited file through QA and approve it; send the current approved version; the approval was withdrawn, ask who withdrew it and why. Nobody has to open SendLint to find out why the send stopped, and the certificate’s public verify page shows anyone with the link what was approved.
The same gate from anywhere else
The gate is one HTTPS GET, so it works from any pipeline that can hash a file and make a request: a CI job before a deploy, a Braze or Marketo webhook, a release script. Two lines in a shell:
curl -sf -H "Authorization: Bearer $SENDLINT_KEY" "https://sendlint.com/api/v1/verify?hash=$HASH"
Keys are the same self-serve API keys as the analyze endpoint, created under API Keys in the app and scoped to your workspace. How certificates are minted, superseded and revoked, and what the public verify page shows, is in proving the sent email matches what was approved.
An honest status note
The SendLint side of this recipe (certificates, the public verify page and the verify API) is live and tested; the Script Activity’s contract is proven against the live API by a harness that replays every outcome a send pipeline must handle: the approved file, the same file with the other line endings, a one-character edit, a missing key. The SFMC steps use stock platform features (Script Activities, Data Extensions, HTTP requests) that your Marketing Cloud team already knows, but every business unit is configured differently: treat the recipe as the integration pattern, and validate the automation in your own business unit before relying on it for a production send.
Frequently asked questions
- How do you gate a Salesforce Marketing Cloud send on QA approval?
- Add a Script Activity ahead of the Send Email activity in Automation Studio. It hashes the approved HTML with SHA256(), calls SendLint's verify API with that fingerprint, and throws unless the response says the exact file is on a standing certificate. The throw stops the automation, so the send never fires on an unverified file.
- What happens if the email is edited after approval?
- Any changed byte produces a different SHA-256, so the verify API answers verified false with status not_found and the automation stops. The fix is to run the edited file as a new round in SendLint and approve it, which mints a fresh certificate; the old one is marked superseded.
- Does personalisation break hash verification?
- No, as long as you hash the same template bytes SendLint analysed, before merge-field substitution and before SFMC rewrites the links for tracking. Personalisation strings like %%FirstName%% are part of the template and hash consistently.
- Why does the gate retry with different line endings?
- Line endings are not content: a browser upload converts line breaks to CRLF and SMTP sends CRLF regardless, so the same email can exist with CRLF or LF endings and two different hashes. On a not_found answer the gate hashes the other form and asks once more. A superseded or revoked answer is final and is never retried.
- What should I check in an SFMC test send that the Content Builder preview does not show?
- The tracking-wrapped links and where they land, the subject line and preheader per version, the unsubscribe and profile centre links, the view-as-webpage link, dynamic content resolved for each version and language, the physical mailing address, and images loading from the SFMC content delivery domain. Save the test send as an .eml and run it through QA; the build in Content Builder is not what the recipient gets.
- Does the same gate work outside Salesforce Marketing Cloud?
- Yes. The gate is one HTTPS GET with a hash and an API key, so any CI job, deploy script, Braze or Marketo webhook, or a two-line shell script can call it: hash the file with openssl or shasum, curl the verify endpoint, and stop on anything but verified true.
Keep reading
- How to prove the email you sent is the email that was approvedApproval usually lives in a screenshot and a thread. Here's how to bind it to the exact bytes instead, and verify any file against it later.
- The email QA checklist: every check before you sendEvery check a bank-grade email QA pass covers, why each exists, and which ones humans consistently miss.
- How to QA an email against the copy deck, line by lineThe shapes a deck arrives in, both directions, per version and per language, the parts everyone skips (subject line, alt text, links, tokens), what counts as different, and what to do when the deck is revised mid-campaign.