SendGrid
SendGrid substitutions render unreplaced when legacy tags are mixed with Handlebars templates. Match the template type to the data field and syntax.
Updated Jul 1, 2026
The short answer
"SendGrid substitutions not working" means your placeholder tags ship to recipients unreplaced or blank. The usual cause is mixing the two incompatible systems: legacy substitution tags (the personalizations.substitutions object / SMTP API "sub") only render in classic templates, while dynamic templates (a "d-" template ID) require Handlebars {{variables}} fed by dynamic_template_data. Match the template type to the data field and the wrapper syntax.
This is not an SMTP reply code or a delivery failure — the email sends successfully, but your placeholders arrive as literal text (-name-, %name%, {{name}}) or as empty strings. In almost every case it comes down to one of these:
1. Mixing dynamic templates with legacy substitutions (the #1 cause). SendGrid has two parallel personalization systems, and they do not interoperate:
d-) use the Handlebars templating language. They are populated by the dynamic_template_data object and reference {{ variables }}.personalizations[].substitutions (Web API v3) or the "sub" object of the X-SMTPAPI header (SMTP API). SendGrid's Handlebars docs note these systems coexist: if you prefer your own templating system, you can still insert dynamic data using substitution tags.If you send a d- template but pass substitutions, nothing renders — the dynamic template does not consume the substitutions object. Conversely, passing dynamic_template_data to a classic template, or putting {{var}} Handlebars tags in content sent with substitutions, also fails.
2. Wrong or mismatched wrapper syntax. Legacy substitution tags are commonly written as -tagName-, with %tagName% and #tagName# available as alternate wrappers (which wrapper is best can depend on your library, language, or intermediate mail servers). Handlebars uses {{ tagName }}. The tag in your content and the key in your data must use the exact same wrapper. %name% will never be replaced by a {{name}} value, and vice versa.
3. Non-string values. SendGrid substitution values must be strings. The official docs warn: "Any other type could result in unintended behavior." Passing a number, boolean, array, or object where a string is expected can leave the tag unreplaced.
4. Spaces inside the tag, or tags nested in substitutions. "Do not use spaces inside your substitution tags" — %first name% will not resolve. And "Do not nest substitution tags in substitutions as they will fail and your substitution will not take place." (SendGrid also documents that you can have up to 4 nested substitutions when one substitution value references another tag — but nesting a tag inside a substituted value in the way just described is not supported.)
5. HTML-sensitive characters get encoded. SendGrid advises avoiding characters with special meaning in HTML such as <, >, and &, which "might end up encoded and will not be properly substituted." In Handlebars, double braces {{ }} HTML-escape the value; use the triple-stash {{{ }}} only when you intentionally want raw HTML.
6. Substitutions at the wrong level. With Web API v3, substitutions is documented only as a field inside each object of the personalizations array — it is not part of the schema at the message root. A substitutions object placed outside of personalizations[] is not part of the documented v3 schema and will not be applied as expected.
Step 1 — Identify which system you are on. Look at your request body. Is there a template_id that starts with d-? Then you are on dynamic templates and must use Handlebars.
Step 2 — Match the data field to the template type.
Dynamic template (correct):
{"from": { "email": "you@example.com" },"personalizations": [{"to": [{ "email": "user@example.com" }],"dynamic_template_data": { "firstName": "Ben", "company": "Acme" }}],"template_id": "d-xxxxxxxxxxxxxxxx"}
…with the template content using <p>Hello {{ firstName }}</p>.
Legacy substitutions (correct):
{"from": { "email": "you@example.com" },"personalizations": [{"to": [{ "email": "user@example.com" }],"substitutions": { "-firstName-": "Ben", "-company-": "Acme" }}],"subject": "Hi -firstName-","content": [{ "type": "text/html", "value": "<p>Hello -firstName-</p>" }]}
Step 3 — Verify the wrapper matches. The literal string of the key (including its wrapper characters) must appear verbatim in your subject/body. If your client library sets substitutionWrappers (e.g. the Node.js library defaults to {{/}}), align your content and keys to that.
Step 4 — Cast every value to a string. Wrap numbers/booleans before sending. For Handlebars missing-key blanks, confirm the JSON key path exactly matches the {{ path }} (e.g. {{ user.profile.firstName }} requires that full nested object).
Step 5 — Remove spaces, avoid nesting tags in values, and keep total substitution data under 10,000 bytes per personalization object. SendGrid's Mail Send API reference caps substitutions at 10,000 bytes per personalization object (max 10,000 properties); a separate SendGrid guide page cites a looser 50,000-byte figure, but the API reference is authoritative and enforced.
If you send through Courier to SendGrid, Courier maps your template variables into the correct field automatically, which sidesteps the legacy-vs-dynamic mismatch — but the same string-only and exact-key-match rules still apply to the data you pass Courier.
References
FAQ
The tag in your content was never matched to a value. Most often you are sending a dynamic template (template ID starting with d-), which ignores the substitutions object and only renders Handlebars {{name}} fed by dynamic_template_data. Either switch the content to a classic template using substitutions, or move your data into dynamic_template_data and use {{ }} tags.
One API, every provider
Courier connects to your email, SMS, and push providers, handles retries and failover, and surfaces delivery errors in plain language.
Last reviewed Jul 1, 2026. Courier is not affiliated with third-party providers; error behavior may vary by implementation.
© 2026 Courier. All rights reserved.