Migration

Bringing templates over from another provider

Most template HTML ports across unchanged. The parts that do not are the parts the old provider was quietly adding or rewriting for you, and they fail in ways that look like your template is fine.

Read this if you are moving templates from SendGrid or a provider like it. The specifics below name SendGrid because it is the most common origin, but the three categories apply to any migration.

What ports cleanly, so you know what to look at

The HTML itself is portable. Tables, inline styles, media queries, the whole defensive layout vocabulary email demands: none of it is provider-specific and none of it needs changing. If your template renders in the old provider's preview, the markup will render here.

The engine is Handlebars, and the built-in helpers work as you expect them to. Conditionals, loops including nested ones, {{this}}, {{@index}}, {{#unless}}, {{#with}} and {{lookup}} all behave normally. Five Liquid-style pipe filters are converted automatically as a courtesy, so {{ name | upcase }} keeps working alongside prepend, append, default and downcase.

So the migration is not a rewrite. It is a review of three specific things, each of which fails silently if you skip it.

Strip the unsubscribe footers out of transactional templates

This is the one that catches almost everybody, and it catches them in the direction that looks fine. Providers commonly require an unsubscribe token in every template, so transactional templates in the old system often carry an unsubscribe footer that was never really appropriate for them. Ported across, that footer becomes an empty link.

The reason is that we supply the unsubscribe variable only when the policy actually owes one. A transactional send owes nothing, because transactional mail deliberately ignores marketing opt-outs, so a template referencing {{unsubscribe_url}} on that path renders it as an empty string. The recipient gets a receipt with a link that goes nowhere.

The preview catches this for you and names it. Run the template through POST /templates/preview and check unsubscribeVariableIgnored in the response: it is set precisely when the template references the variable and its policy never supplies it. Fix it by deleting the footer from transactional templates, which is where it did not belong anyway.

The mirror image is worth stating too. In a MARKETING template, {{unsubscribe_url}} is how you choose where the opt-out sits, and only a link to that URL counts. A ported footer pointing at your own site looks like compliance without being it, so we append the standard footer as well and set hasUnrecognizedUnsubscribeLink to tell you why there are suddenly two.

Variables are case-sensitive, and a miss renders empty

A referenced variable that does not exist renders as an empty string rather than raising an error. That is the right behaviour for email, where a hard failure mid-send helps nobody, and it is a trap during a migration because a subtly wrong name produces a template that renders perfectly and says nothing.

Case is the usual culprit. {{firstName}} and {{firstname}} are different variables, and if the old provider was case-insensitive or normalised names for you, every one of those becomes a blank on the first send. Audit the names against the data you are actually passing rather than against the old template.

Preview with real sample data instead of eyeballing the markup. Pass sampleData to the preview endpoint with the payload your code will genuinely send, and read the rendered output for blanks. A blank where a name should be is the whole class of bug, and it is visible in one glance.

One more difference worth knowing: {{var}} HTML-escapes its value and {{{var}}} does not. If a ported template renders escaped markup where you expected formatting, it wants the triple form, and if it renders raw user input, it should not.

Clean out the old provider's tracking

Exported HTML frequently carries the previous provider's machinery baked in: links already rewritten through their click-tracking redirect, an open-tracking pixel pointing at their domain, and campaign attribution parameters such as ?ref= or utm_ values from whichever send the export was taken from.

Rewritten links are the serious one. A link pointing at your old provider's redirect will keep working only for as long as that account exists, and it attributes the click to a system you no longer use. Replace them with the real destination URLs.

The old pixel is dead weight that loads a third party for every recipient. Delete it. We add our own open tracking on both streams, and on transactional sends we deliberately do not rewrite links at all, so a reset link stays exactly as you wrote it and does not depend on a redirect of ours being up.

Stale attribution parameters are the quiet one: they do not break anything, they just permanently misreport where your traffic came from. Strip them and let your analytics see the truth.

Before you save any of them

Two mechanical things will stop a ported template at the door, and both are quicker to know than to debug.

  • Creating a template requires emailType. There is no default any more, deliberately: the type decides which suppression rules and unsubscribe machinery every future send gets, and a receipt template that silently defaulted to marketing was a real hazard.
  • Changing the type later needs confirmEmailTypeChange: true. Reclassifying is a compliance event, so it is never a side effect of an edit that was meant to fix a typo.
  • Preview through the channel you will really send on. A TRANSACTIONAL template previewed as a campaign shows the unsubscribe block, because the channel sets a floor the template cannot lower.

When this does not apply

Do not port a template you were already unhappy with. A migration is the cheapest moment to delete the three campaigns nobody opens and rewrite the onboarding email you have been meaning to fix, because you are touching all of them anyway. Carrying a bad template across costs the same as carrying a good one and keeps the problem.

Questions

Is the engine Handlebars or Liquid?
Handlebars. Five Liquid-style pipe filters are converted automatically as a convenience (prepend, append, default, upcase, downcase), which covers the most common Liquid usage in ported templates, but the engine underneath is Handlebars and its helpers are what is available.
What happens if a ported template will not compile?
The send is refused with a 400 and the code templates.renderFailed, and the preview fails the same way. That is deliberate: the alternative is mailing the raw template source to a customer, which some systems do.
Is there a bulk import for templates?
No. Each one is a POST to the templates endpoint, which is straightforward to script over an export, and the SDK wraps it. There is no drag-and-drop importer and we would rather say so than imply one exists.
Can I run both providers during the migration?
Yes, and it is the sensible way to do it. Move one template at a time and keep the old system sending until you have seen the new one work. The thing to watch is suppression state, which does not sync between providers: an address that unsubscribed in the old system is not suppressed here until you tell us about it.

Read next

Private beta

Guides are free. The API needs a key.

Join the list and your onboarding email carries a scoped key and the base URL, so you can follow this end to end.