Transactional emails

The three emails Fontdue sends your customers, how to move them to your own email provider, and every value their templates receive

A transactional email is one Fontdue sends to a customer because something happened in your store – an order was paid, a payment failed, a customer asked for a link back into their account. There are three of them. They’re branded as your foundry rather than as Fontdue, and by default they go out through Fontdue’s own SendGrid account using Fontdue’s templates.

Connecting your own email provider changes that. Fontdue sends your provider a template id and a JSON payload, and your template decides what the customer sees – Fontdue never sends rendered HTML for these three.

The three emails

Each email has its own payload, and all three share a common branding block.

Email Sent when
Order receipt An order is paid, and whenever you re-send a receipt by hand
Payment failed A payment attempt on an order fails
Customer dashboard login link A customer asks for a link to their account dashboard

The emails about your Fontdue account – verifying your address, resetting your password, inviting a colleague – aren’t in this list. They come from Fontdue rather than from your foundry, and they aren’t customizable.

On Fontdue’s templates the design is fixed and only a handful of settings reach into it: the logo and fonts from Settings → Branding, the receipt and invoice memos from Settings → Receipts, and the address they send from, set under Settings → Email domain. Layout, wording and colour need your own provider.

Choosing a provider

Three providers can be connected under Settings → Emails, and all three work the same way: you build the templates in the provider’s own editor, and Fontdue calls its API with a template id and the payload.

Provider Template language
SendGrid Handlebars
Postmark Mustachio
Brevo Jinja

All three handle the payload the same way in the ways that matter – they walk nested objects, repeat a block once per array entry, and treat a value that arrived as null as blank. Only the syntax differs, and template syntax below shows the same fields written three ways.

Other providers

Those three were chosen because they can render these payloads. Two well-known alternatives can’t, and both fail on the payload rather than on anything about Fontdue.

Resend’s templates take only string and number variables, with no way to repeat a block – so a receipt listing more than one font can’t be built.11Resend is also strict about variables it can’t resolve: a template that declares one with no fallback fails the send outright when the value is missing, rather than rendering a blank. Klaviyo has no plain send-this-template endpoint left; transactional mail goes through an event that triggers a flow you build in Klaviyo, which never reports back whether the message went out – so the fallback below couldn’t work. Every recipient also becomes a billable profile.22This is only about transactional email. Klaviyo works as a newsletter destination and as an analytics destination – separate features, separate settings, and they can share one Klaviyo account.

Mailgun and Amazon SES could be added. Get in touch if you use one of them.

Authenticating your sending domain

The from address you set under Settings → Emails has to be a verified sender in your provider’s account, or that provider rejects every send. Each of them explains how when you sign up – usually verifying a single address to get started, then a set of DNS records that authenticates the whole domain.

Authenticating a domain under Settings → Email domain is a separate thing, and doesn’t cover it. That screen adds your domain to Fontdue’s SendGrid account, so it covers only the emails Fontdue sends; an email that goes live through your own provider is signed by that provider instead, including when both are sending from the same address.33Authenticating the same domain with both services is fine – a domain can be authenticated with as many as you like, and the DNS records sit alongside each other rather than replacing one another. The Email domain screen lists which emails have moved, so you can see what it’s still responsible for.

Moving one email at a time

Each email moves separately, and each is in one of three states:

  • Fontdue’s template – the default. No template id set.
  • Draft – a template id is set but not live. Real emails still go out through Fontdue; test sends go to your provider, so you can work on the template without customers seeing it.
  • Live – real emails go out through your provider, using your template.

Send a test works in all three states, and before you’ve set anything up at all: it sends whichever template the screen is currently pointed at – yours, or Fontdue’s – so it’s also how to look at the email your customers receive today. It reads the form rather than what’s saved, so a template id you’ve just pasted can be tried before you commit to it, and the dialog names the provider and template id it’s about to use.

1 Connect your provider

Under Settings → Emails, switch on Send through my own email provider, choose your provider, and paste an API key with permission to send mail. Add the from address – verified in your provider account – and save.

2 Build a template

In your provider, create a template for one email and write it against the payload below, which lists every value available.

Starting from Fontdue’s design is often easier than starting from scratch: send yourself a receipt from the order screen, then work from that.

A field that comes out blank is usually a path that didn’t resolve rather than a value Fontdue didn’t send – template languages return nothing rather than complain. Reaching an outer field from inside a loop covers the case that catches people out.

3 Paste the template id

Back on Settings → Emails, put the template id against that email, leaving Send this email through my provider unchecked. Once saved, the email shows as a draft.

4 Send yourself a test

Use Send a test to send that email through your provider to your own address. Tests use example order data and your real branding, so this works before your store has taken an order.44Because the data is an example, the figures in a test receipt won’t match a real order – it’s the design you’re checking, not the numbers. Fix and re-test until it looks right – nothing has to be saved between attempts.

5 Switch it live

Tick Send this email through my provider and save. From that point real sends use your template. Repeat for the next email whenever you’re ready.

When a send fails

If your provider rejects a live send – an expired key, an unverified address, a deleted template – Fontdue sends the email through its own template instead. A customer getting their receipt in Fontdue’s design is better than not getting one, and the receipt is the record of their purchase.

Turning off Send through my own email provider puts every email back on Fontdue immediately, without clearing your template ids – so it’s a safe thing to do if something looks wrong and you want to stop and investigate.

A test send is the exception: when it goes through your provider it reports that provider’s own error rather than falling back, which is what makes it the place to diagnose a misconfiguration.55A test only reaches your provider once there’s a template id for that email and the provider details are complete. Short of that it sends Fontdue’s template instead, and says so before you send it – worth reading, since a Fontdue email arriving looks much like your own template working.

The payload

Money values are pre-formatted strings without a currency symbol, already using the separators for the order’s currency – "1,234.56" in dollars, "1.234,56" in euros. The symbol arrives separately in currency. So a template writes the two together rather than formatting a number itself.66This means a template can’t do arithmetic on a total – there’s no raw number to work with. Every figure a receipt needs is already in the payload as its own field.

Dates are strings in YYYY-MM-DD, in your foundry’s timezone.

Any field can arrive as null. A foundry with no logo has logo_url: null; an order with no coupon has coupon_code: null. Write templates so a missing value degrades gracefully rather than printing an empty box.

Branding block

Every one of the three payloads carries these, so a shared header and footer can be written once and used in all three templates.

Field Type Example
foundry object see the foundry object
logo_url string | null "https://example.com/logo"
logo_width number | null 120.0
ui_font_url string | null "https://fonts.fontdue.com/…/css/1.css"
ui_font_name string | null "Tomka"

logo_width is half the logo’s real pixel width, so a logo uploaded at twice the intended size renders correctly on high-density screens. Set the image’s width to this value.

ui_font_url is a stylesheet, not a font file – link it in the template’s <head> and then use ui_font_name as the font-family. Mail clients vary in whether they honour webfonts, so always give the family a fallback.

The foundry object

foundry is your own details – the name, address and contact information a customer needs to see on a receipt. None of it is anything about Fontdue.

Field Type Example
name string "Example Type Foundry"
hostname string "example.com"
url string "https://example.com"
address_formatted string | null "Example Type LLC\n123 Foo St\nNew York NY 10000\nUnited States"
email string | null "[email protected]"
phone string | null "+1 212 234 5678"
tax_ids array [{"type": "VAT", "value": "FR12345678901"}]

address_formatted is a single string with newlines, already ordered for the country it’s in. Render it with white-space: pre-line, or split on newlines and emit a line each. Whether the address and phone appear at all follows the Include address and Include phone settings on Settings → Receipts.

tax_ids is a list because a company can be registered in more than one country. type is already the label to print – VAT, GST, ABN, EIN and so on – rather than a raw registration type, so looping over it and emitting type and value gives you VAT: FR12345678901 with no mapping of your own.77The list is empty rather than absent when there’s nothing to show, so a loop over it renders nothing and needs no guard around it.

Order receipt

The largest payload. Alongside the branding block it carries the order itself.

Field Type Example
id number 70001
invoice_number string | null "000123"
description string "# Company size: 2–10 employees\n● Tomka Regular – Desktop"
date string | null "2026-07-30"
currency string | null "€"
order_items array see line items
order_variable_selections array see line items
subtotal string "840.00"
discount string | null "84.00"
coupon_code string | null "LAUNCH10"
discounted_subtotal string "756.00"
taxes string | null "151.20"
tax_name string | null "VAT"
tax_rate string | null "20%"
tax_inclusive boolean false
tax_not_applicable boolean false
tax_not_applicable_reason string | null "Reverse charge"
is_reverse_charge boolean false
total string "907.20"
payment_amount string "907.20"
payment_method string | null "Visa - 4242"
stripe_charge_id string | null "ch_3Example"
refunded boolean | null false
customer object {"name": "Alex Rivera", "email": "[email protected]"}
billing object | null see identities
licensee object | null see identities
completed_with_coupon boolean false
completed_externally boolean false
invoice_name string "Invoice"
receipt_memo string | null "Thanks for your order."
archive_url string "https://example.com/orders/70001/archive"
invoice_url string "https://example.com/orders/70001/invoice.pdf"

archive_url and invoice_url carry a signed token, so they work without the customer signing in. They’re the two links a receipt has to include – the fonts themselves and the invoice PDF.

description is a single multi-line summary of the whole order, not a title – each order variable on its own line prefixed # , then each licensed font prefixed . It’s a convenience for a plain-text part or a subject line; a receipt that lays out its own table wants order_items and order_variable_selections instead.

invoice_number is normally zero-padded to six digits. A foundry that needs a different format – a prefix, the year – can ask us to set one up, in which case this field carries that instead.

tax_rate arrives as a display string with a percent sign already attached, not a number.

total is what the order came to; payment_amount is what was actually charged. They’re normally the same, and differ when a payment was taken separately from the order total.

Line items

order_items is one entry per licensed font, with the price already multiplied by any order-variable multipliers – so the prices sum to subtotal without further arithmetic.

Field Type Example
id number 90001
description string "Tomka Regular – Desktop"
price string "240.00"

order_variable_selections is one entry per choice the customer made at checkout – company size, traffic tier, and so on. These are what drove the multipliers above, and are worth printing so a customer can see what they licensed for.

Field Type Example
id number 80001
description string "Company size: 2–10 employees"

Both arrays need a template that can repeat a block per item – see template syntax for how each language spells that.

Identities

billing is who paid; licensee is who the licence is for. They’re the same shape, and licensee is null when the customer licensed the fonts for themselves. address_formatted and tax_ids work exactly as they do in the branding block.

Field Type Example
address_formatted string | null "Alex Rivera\n14 Rue de Rivoli\n75001 Paris\nFrance"
email string | null "[email protected]"
phone string | null "+33 1 23 45 67 89"
tax_ids array [{"type": "VAT", "value": "FR12345678901"}]

Payment failed

The branding block, plus what the customer was trying to pay.

Field Type Example
total string "907.20"
currency string | null "€"

Customer dashboard login link

One field, alongside the branding block.

Field Type Example
customer_dashboard_url string "https://example.com/dashboard?token=…"

The link contains a single-use signed token. It’s the whole point of the email, so a template must put it somewhere the customer can reach it, and this one shouldn’t be sent to anyone but the addressed customer.88Because the token grants access to a customer’s account, the payload for this email is worth treating as sensitive – it passes through your provider’s logs like any other send.

Template syntax

The payload is the same whichever provider you use. Reading the same fields across the three:

{{! SendGrid }}
{{foundry.name}}{{currency}}{{total}}
{{#each order_items}}<tr><td>{{description}}</td><td>{{price}}</td></tr>{{/each}}
<!-- Postmark -->
{{foundry.name}} – {{currency}}{{total}}
{{#each order_items}}<tr><td>{{description}}</td><td>{{price}}</td></tr>{{/each}}
<!-- Brevo -->
{{ params.foundry.name }} – {{ params.currency }}{{ params.total }}
{% for item in params.order_items %}<tr><td>{{ item.description }}</td></tr>{% endfor %}

Postmark’s Mustachio looks like Mustache but isn’t: a bare {{#order_items}}…{{/order_items}} section enters the block once instead of repeating it, and {{#if …}} isn’t valid at all. Use {{#each}} to repeat, and a plain {{#coupon_code}}…{{/coupon_code}} section as the conditional – inside which the scope has already moved, so the value itself reads {{../coupon_code}}.

Reaching an outer field from inside a loop

A receipt usually wants currency next to each line item’s price, and currency sits at the top level while price belongs to the item. Each language spells that differently.

<!-- Brevo: everything stays under params, so nothing special is needed -->
{% for item in params.order_items %}{{ params.currency }}{{ item.price }}{% endfor %}
<!-- Postmark: each pushes two scopes, so the top level is two levels up -->
{{#each order_items}}{{../../currency}}{{price}}{{/each}}
{{! SendGrid: each pushes one scope }}
{{#each order_items}}{{../currency}}{{price}}{{/each}}

Postmark’s two levels are the one worth remembering – {{../currency}} looks right and renders empty, with no error, so a price silently loses its currency symbol.99Postmark’s template editor validates paths as you type and flags an unknown one, but a path that resolves to nothing is not an error, so this particular mistake gets through.

Changes to the payload

New fields get added to these payloads over time. Fields are never renamed or removed, and a field’s type never changes – so a template written against this page keeps working, and a payload gaining a field your template ignores changes nothing.

If you want a value that isn’t here, ask us and we’ll look at adding it.

1 Resend is also strict about variables it can’t resolve: a template that declares one with no fallback fails the send outright when the value is missing, rather than rendering a blank. 
2 This is only about transactional email. Klaviyo works as a newsletter destination and as an analytics destination – separate features, separate settings, and they can share one Klaviyo account. 
3 Authenticating the same domain with both services is fine – a domain can be authenticated with as many as you like, and the DNS records sit alongside each other rather than replacing one another. 
4 Because the data is an example, the figures in a test receipt won’t match a real order – it’s the design you’re checking, not the numbers. 
5 A test only reaches your provider once there’s a template id for that email and the provider details are complete. Short of that it sends Fontdue’s template instead, and says so before you send it – worth reading, since a Fontdue email arriving looks much like your own template working. 
6 This means a template can’t do arithmetic on a total – there’s no raw number to work with. Every figure a receipt needs is already in the payload as its own field. 
7 The list is empty rather than absent when there’s nothing to show, so a loop over it renders nothing and needs no guard around it. 
8 Because the token grants access to a customer’s account, the payload for this email is worth treating as sensitive – it passes through your provider’s logs like any other send. 
9 Postmark’s template editor validates paths as you type and flags an unknown one, but a path that resolves to nothing is not an error, so this particular mistake gets through.