SendGrid dynamic template data renders blank when key names don't match Handlebars variables, data is outside personalizations, or a legacy ID is used.
Updated Jul 1, 2026
The short answer
"SendGrid dynamic template data not working" means your email sends but the Handlebars variables render blank or literal instead of your values. The usual causes are mismatched field names (the JSON key must exactly match the {{handlebar}}, alphanumeric/underscore only), dynamic_template_data placed outside personalizations, using a legacy template ID instead of a d- dynamic ID, or mixing substitutions with dynamic templates.
"Dynamic template data not working" is not an SMTP reply code or a named SendGrid API error — it's the common description of a successful send (HTTP 202 Accepted) in which the Handlebars variables in your dynamic template come out empty or appear as literal {{text}} instead of your data. Because the API returns 202, nothing fails loudly; the breakdown is in how the template and the dynamic_template_data payload line up. Below are the concrete causes, most common first.
dynamic_template_data. {{firstName}} will not be filled by first_name. Missing or misspelled keys render as empty strings silently (Using Handlebars).{{first name}} saves without an error but never substitutes (Handlebars Syntax Errors).dynamic_template_data in the wrong place. It must live inside each personalizations object, not at the top level of the request body. Top-level data is ignored.template_id must be a Dynamic template ID — it starts with d- followed by a 32-character hexadecimal string (a UUID with its dashes removed), for example d-f43daeeaef504760851f727007e0b5d0. A legacy transactional template ID will not interpret Handlebars; legacy templates use substitutions with -tag- syntax instead.substitutions with a dynamic template. These are mutually exclusive. Sending both returns the API error "Substitutions may not be used with dynamic templating."{{unbalanced}}} breaks rendering. Values containing HTML or ' " & are HTML-escaped by {{ }}; use triple braces {{{ value }}} to output raw HTML.Step 1 — Confirm placement and ID. Put dynamic_template_data inside personalizations and use the d-... ID:
{"from": { "email": "you@example.com" },"personalizations": [{"to": [{ "email": "user@example.com" }],"dynamic_template_data": { "first_name": "Ada", "order_id": 1234 }}],"template_id": "d-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
Step 2 — Match names exactly. Open the active version of the template and make every {{key}} match a key in your JSON character-for-character. Keep names to letters, digits, and underscores; no spaces.
Step 3 — Remove conflicts. Delete any substitutions block from the request when using a dynamic template. Use one system or the other.
Step 4 — Test with the editor's preview data. The template editor lets you paste test JSON. If it renders there but not in your send, the data shape differs from your live payload — log the exact body you send. (Note: variables nested inside greaterThan/equals/and/or conditionals can look blank in the editor preview yet render correctly in the delivered email — a known editor-only display bug.)
Step 5 — Handle nested data and HTML. Use dot notation for nested objects ({{user.profile.name}}), {{#each items}}...{{this.name}}...{{/each}} for arrays, and {{{html_block}}} when the value is intended to be raw HTML.
With Courier
dynamic_template_data — Courier maps the data you pass to send() into the template variables. Confirm your SendGrid provider is configured, the variable names in the SendGrid template match the keys in your Courier data object, and that you are referencing the correct dynamic template.References
FAQ
A successful send returns HTTP 202 regardless of whether variables resolved. Blank values mean the keys in dynamic_template_data don't exactly match the {{handlebar}} names, the data was placed outside the personalizations object, or you used a legacy template ID instead of a dynamic (d-) one. SendGrid substitutes missing keys with an empty string rather than erroring.
It means your request included both a substitutions block and a dynamic (d-) template_id. They are mutually exclusive: legacy templates use substitutions with -tag- syntax, while dynamic templates use dynamic_template_data with Handlebars {{ }}. Remove the substitutions block and pass your values only in dynamic_template_data.
Literal {{ }} usually means the template_id is a legacy transactional template (not a d- dynamic template), so Handlebars is never evaluated. It can also mean no template version is set active. Switch to a Dynamic template ID and mark a version active.
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.