Amazon SES Templates are reusable email templates stored server-side in your SES account, keyed by name and merged with per-recipient JSON data at send time. You create one with CreateEmailTemplate, reference it by TemplateName in SendEmail or SendBulkEmail, and SES handles the variable substitution before the message hits the queue. Each Region supports up to 20,000 templates at 500 KB each, with no cap on the number of replacement variables (AWS docs).
This guide walks through the template structure, the Mustache-style merge syntax, the difference between stored and inline templates, and how SendEmail differs from SendBulkEmail in practice. Examples use the SES v2 API via the AWS CLI - the same shapes apply across the SDKs.
What SES Templates are (and what they are not)
SES Templates are a thin server-side merge mechanism for SendEmail and SendBulkEmail. AWS documents two flavours: stored templates created with CreateEmailTemplate and referenced by name, and inline templates passed directly inside the send call. Stored templates are capped at 500 KB and 20,000 per Region; inline templates extend the input payload limit to 1 MB and do not count against the template quota.
What they are not: a full templating engine. There are no conditionals, no loops, no partials, no helpers, no localisation hooks, and no asset pipeline. You get {{tagname}} placeholders and a single flat JSON object of replacement values per recipient. Anything more sophisticated - dynamic product blocks, locale switches, dark-mode CSS variants - has to be rendered in your application before the data reaches SES.
The mental model is closer to AWS Lambda environment variables than to Mailchimp's drag-and-drop editor. SES Templates exist so transactional pipelines can keep subject lines and body copy out of code, not so marketing teams can edit emails.
Template structure: TemplateName, Subject, Text, Html
A stored SES template is a JSON document with two top-level fields - TemplateName and TemplateContent - where TemplateContent carries the Subject, Text, and Html parts (AWS, 2026). The name is the lookup key you reference from SendEmail, and the three content parts hold the rendered output with optional {{tagname}} placeholders inline. The text part is what email clients with HTML disabled fall back to, and AWS documents the same {{tagname}} substitution behaviour in all three.
Here is the canonical shape from the AWS documentation:
{
"TemplateName": "OrderConfirmation",
"TemplateContent": {
"Subject": "Order #{{order_id}} confirmed",
"Text": "Hi {{first_name}},\r\nYour order for {{item}} is on the way.",
"Html": "<h1>Hi {{first_name}},</h1><p>Your order for <strong>{{item}}</strong> is on the way.</p>"
}
}
A few rules that bite teams in production:
- The
Textpart is not optional in any meaningful sense. SES will accept a template without one, but spam filters at the major mailbox providers downgrade single-part HTML emails. Always provide both. Subject,Text, andHtmlare merged independently. A merge tag that exists inHtmlbut not inSubjectis fine; a tag referenced anywhere that is missing fromTemplateDatatriggers a Rendering Failure event.- The 500 KB ceiling covers
Subject+Text+Htmlcombined, after JSON encoding. Inlined base64 images count, which is why most teams keep imagery hosted at a CDN URL and reference it from the HTML.
The Mustache-style substitution syntax
SES uses a deliberately minimal subset of Mustache for variable substitution: {{tagname}} is replaced with the matching key from your TemplateData JSON, and that is the entire feature set. There are no sections ({{#each}}), no inverted sections ({{^missing}}), no partials ({{> header}}), and no helpers. AWS does not impose a cap on the number of replacement variables per template, so the constraint is the 500 KB total size, not tag count.
Tag names must match the JSON key character-for-character. A few practical rules:
- Whitespace inside the braces is ignored:
{{ first_name }}and{{first_name}}both look up thefirst_namekey. - Values are inserted as-is. SES does not HTML-escape replacement values, so untrusted input belongs through your own sanitiser before it reaches
TemplateData. - A missing key produces a Rendering Failure - not a silent empty string. The message is accepted by the API and then dropped at render time, which is invisible unless you have event publishing configured.
- Nested objects do not work.
{{user.name}}is treated as a literal tag nameduser.name, not as a path lookup. Flatten your data before sending.
If you need conditional content or iteration over an array of line items, render the HTML in your application code and pass it to SendEmail as raw Content instead of leaning on SES Templates.
Creating templates via the SES v2 API
Templates are created with the CreateEmailTemplate operation in the SES v2 API, capped at one call per second per account (AWS, 2026). The CLI accepts the JSON payload via --cli-input-json and returns an HTTP 200 with an empty body on success. Updates use UpdateEmailTemplate, deletes use DeleteEmailTemplate, and GetEmailTemplate returns the stored content for inspection. There is no versioning, so an update overwrites the template in place with no rollback.
Save the template JSON to a file (call it order-confirmation.json):
{
"TemplateName": "OrderConfirmation",
"TemplateContent": {
"Subject": "Order #{{order_id}} confirmed",
"Text": "Hi {{first_name}},\r\nYour order for {{item}} is on the way.\r\nTracking: {{tracking_url}}",
"Html": "<h1>Hi {{first_name}},</h1><p>Your order for <strong>{{item}}</strong> is on the way.</p><p><a href=\"{{tracking_url}}\">Track your shipment</a></p>"
}
}
Create it:
aws sesv2 create-email-template \
--cli-input-json file://order-confirmation.json
Inspect it later:
aws sesv2 get-email-template --template-name OrderConfirmation
List everything in the Region:
aws sesv2 list-email-templates --page-size 50
Templates are scoped per AWS Region. A template named OrderConfirmation in us-east-1 is invisible from eu-west-1, which catches teams running multi-Region failover. Replicate templates through your IaC pipeline rather than assuming SES will sync them.
SendEmail vs SendBulkEmail
SendEmail sends one merged message to a single Destination object; SendBulkEmail sends up to 50 unique merged messages in a single API call. The trade-off is throughput against per-recipient flexibility - SendBulkEmail lets you supply distinct ReplacementTemplateData per destination, but only up to 50 destinations per call. Both operations accept either a stored template (by TemplateName) or an inline template (with TemplateContent embedded in the request).
A single-recipient send with a stored template looks like this:
{
"FromEmailAddress": "shop@example.com",
"Destination": {
"ToAddresses": ["customer@example.com"]
},
"Content": {
"Template": {
"TemplateName": "OrderConfirmation",
"TemplateData": "{ \"first_name\": \"Anaya\", \"order_id\": \"10428\", \"item\": \"Kettle\", \"tracking_url\": \"https://track.example.com/10428\" }"
}
},
"ConfigurationSetName": "transactional"
}
Note that TemplateData is a stringified JSON blob, not a nested object. That requirement is consistent across the SDKs - the AWS SDK for JavaScript, Python, Go, and Ruby all want a string here, not a hash.
A bulk send fans out:
{
"FromEmailAddress": "shop@example.com",
"DefaultContent": {
"Template": {
"TemplateName": "OrderConfirmation",
"TemplateData": "{ \"first_name\": \"there\", \"item\": \"your order\" }"
}
},
"BulkEmailEntries": [
{
"Destination": { "ToAddresses": ["anaya@example.com"] },
"ReplacementEmailContent": {
"ReplacementTemplate": {
"ReplacementTemplateData": "{ \"first_name\": \"Anaya\", \"order_id\": \"10428\", \"item\": \"Kettle\" }"
}
}
},
{
"Destination": { "ToAddresses": ["liu@example.com"] },
"ReplacementEmailContent": {
"ReplacementTemplate": {
"ReplacementTemplateData": "{ \"first_name\": \"Liu\", \"order_id\": \"10429\", \"item\": \"French Press\" }"
}
}
}
],
"ConfigurationSetName": "transactional"
}
Two behaviours worth remembering:
DefaultContent.Template.TemplateDatais the fallback. If aBulkEmailEntriesentry passes"ReplacementTemplateData": "{}", SES uses the defaults for that recipient.- The 50-destination cap is per call, not per second. Your account's sending rate still governs how fast you can call
SendBulkEmail. For a 10,000-recipient campaign, that is 200 calls scheduled against your account rate.
Limits, quotas, and pricing
The hard limits on SES Templates are tight but predictable. AWS documents 20,000 stored templates per Region, 500 KB per stored template, 1 MB per inline template payload, and 50 destinations per SendBulkEmail call. Templating itself is free - SES does not charge for CreateEmailTemplate or for the merge step; you pay only the standard per-1,000-email fee plus data transfer.
| Quota | Stored templates | Inline templates |
|---|---|---|
| Per-template size | 500 KB (Subject + Text + Html) | 1 MB (full input JSON) |
| Templates per Region | 20,000 | Not applicable |
| Replacement variables | No documented limit | No documented limit |
| SendBulkEmail destinations | 50 per call | 50 per call |
| Storage cost | Free | Not stored |
The 50-destination cap on SendBulkEmail is the limit most teams notice first. A pipeline that batches 1,000 recipients per call has to be re-architected. The 500 KB template ceiling is the second - inlining a 600 KB hero image as base64 will quietly fail CreateEmailTemplate with a LimitExceededException.
Common pitfalls
Two failure modes account for most production incidents with SES Templates: missing merge variables and unescaped characters in TemplateData. Both surface as Rendering Failure events rather than API errors, so without an SNS event destination on a configuration set the affected messages disappear with no trace in SendEmail responses (AWS, 2026). The SendEmail call returns HTTP 200 with a MessageId, the render step fails downstream, and the recipient never gets the email.
Concrete things to watch for:
- Unescaped quotes in
TemplateData. The field is a stringified JSON blob, so a value containing"must be escaped as\". Building this string with naive concatenation breaks on any name with an apostrophe. - Missing variables. A template that references
{{coupon_code}}will fail rendering ifTemplateDataomits the key. The message is accepted bySendEmail(HTTP 200 with aMessageId) and dropped silently at the render stage. - HTML injection. SES does not escape replacement values. If
first_nameis<script>alert(1)</script>, that string lands in the HTML body verbatim. Sanitise upstream. - Region mismatch.
SendEmaillooks upTemplateNamein the Region the API call hits. Callingus-east-1for a template created ineu-west-1returnsTemplateDoesNotExist. - Stale templates.
UpdateEmailTemplateis a destructive overwrite. There is no version history and no rollback. Keep the canonical copy in version control.
The recommended setup is a configuration set with event publishing wired to capture RenderingFailure events so you can detect and replay broken sends.
When to use SES Templates vs your own templating
Use SES Templates when your transactional pipeline is simple, the body copy needs to live outside application code, and the marketing team does not need to edit emails. Roll your own templating when you need conditionals, loops, locale variants, or a WYSIWYG editor - render the final HTML in your application and call SendEmail with raw Content.
Concrete decision points:
- Choose SES Templates if your email is a transactional notification with five to twenty merge fields, you want subject lines configurable without a deploy, and your dev team owns the content lifecycle.
- Roll your own if you need
{{#each items}}over a line-item array, locale switches, A/B subject lines, or a non-technical editor. - Hybrid is common: render rich blocks server-side, then pass the resulting HTML through
SendEmailwith a thin layer of SES merge tags for first name and unsubscribe link.
How Mailblast handles templates
Mailblast does not use SES Templates as its primary editing surface. Instead, the drag-and-drop editor (powered by BeeFree) renders the final HTML in your Mailblast workspace and passes the merged result to your Amazon SES account at send time. You get Liquid-style personalisation, content blocks, button styling, and image uploads in a real WYSIWYG editor - rather than the {{tagname}}-only ceiling of SES Templates - while still delivering through your own SES so the per-1,000 cost stays at AWS rates.
If your team needs stored SES Templates for a separate transactional pipeline (order confirmations, password resets, webhook receipts), Mailblast leaves that side of your SES account untouched. The management layer is scoped to the marketing and broadcast use case; your application code is free to call CreateEmailTemplate and SendEmail directly for transactional sends without interference.
FAQ
What is the size limit for an Amazon SES template?
Each stored SES template can be up to 500 KB total, including both the HTML and text parts. Inline templates passed directly to SendEmail or SendBulkEmail get a larger ceiling: the full input JSON payload can be up to 1 MB. Most teams hit the 500 KB ceiling because of inlined CSS and base64 images, not because of body copy.
How many SES templates can I create per account?
You can create up to 20,000 stored email templates in each AWS Region. Inline templates do not count against this quota because they live in the SendEmail or SendBulkEmail request itself rather than as a Template resource. If you are designing programmatic systems that mint a template per campaign, watch the per-Region cap and clean up unused templates with DeleteEmailTemplate.
Does SES Templates support conditionals or loops like Mustache?
No. SES uses Mustache-style {{tagname}} placeholders for simple key-value substitution only. There is no support for sections, inverted sections, partials, or loops. If you need conditional blocks or iteration over arrays, render the final HTML in your application first and pass it to SendEmail as raw Content rather than relying on SES merge tags.
How many recipients can SendBulkEmail handle in a single call?
SendBulkEmail accepts up to 50 destination objects per call. Each Destination object can contain multiple addresses across ToAddresses, CcAddresses, and BccAddresses, so the per-call recipient count can exceed 50, but only 50 unique merge contexts are possible. For larger sends, batch into multiple SendBulkEmail calls and respect your account's maximum send rate.
What happens if a merge variable is missing from TemplateData?
SES will fail rendering and emit a Rendering Failure event. The message is accepted by the API but never delivered. AWS recommends attaching a configuration set with an SNS event destination that publishes Rendering Failure events so you can detect and re-send affected messages. See our guides on configuration sets and event publishing for setup details.