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.
| 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.
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.
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.
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.
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.
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.