Announcing the Magic Pages Open House. Quarterly calls with our team!

Back to blog

Email Personalization in Ghost

Ghost has 13 email personalization variables and documents none of them. The full list, where the merge tags actually work, and what still doesn't.

· 6 min read

Ghost has 13 email personalization variables. Its own documentation lists none of them.

So the question keeps landing on the forum in one shape or another:

Can you insert Member ID’s into newsletters
We have a need to send out a combination of static content and then per-member customized content. We know how to do this on the website using some JS and cookies and modifying themes but we don’t know how to make this work in the newsletters themselves due to a seeming lack of ability to insert system variables. Note that we’re hosted currently with Ghost Pro and would like to avoid moving the site so that we could hack it to make it do what we need for it to do. That’s just more work for no…

Can you insert a member's ID into an email that Ghost sends? Yes. You can put it in a link, an image URL, or the middle of a sentence, and you can do it without leaving the editor. Nothing on ghost.org tells you that, and a fair amount of what's written about it elsewhere describes a version of Ghost that stopped existing in February 2026.

Here's the whole thing, checked against Ghost 6.59.

Where you meet it first

Most people run into personalization through the email card, the tool that adds email-only content to a post without it showing up on the web:

A screenshot of the email card from the Ghost editor showing "Hey `{first_name, "there"}" as content.

Inside an email card, you write a keyword in curly braces:

{property_name}

You can add a fallback for the case where the property is empty, like a member who never filled in a name:

{property_name, "fallback text"}

That's the part everyone knows. It's also the part that makes people think personalization is an email card feature, which it isn't.

Two steps, and only the first one is about cards

There are two separate mechanisms here, and almost every wrong answer about Ghost personalization comes from treating them as one.

The email card wraps. A single function called wrapReplacementStrings turns {email} into %%{email}%% before the post is rendered. The email card is the only card in the editor that calls it.

The renderer substitutes. It takes the finished HTML of the entire email, scans it for anything matching %%{...}%%, and hands what it finds to Mailgun as recipient variables. It has no idea which card the text came from, and it doesn't look.

So the short form only works in an email card. The long form works anywhere in the post: an HTML card, a plain paragraph, a link, an image URL.

Which is why this, in an HTML card, does exactly what it looks like it does:

<img src="https://example.com/px?u=%%{uuid}%%">

Type the %% yourself and you're out of the email card's jurisdiction.

The 13 variables

They're defined in one place, a method called buildReplacementDefinitions:

Ghost/ghost/core/core/server/services/email-service/email-renderer.js at c40eeac61f17e6e8ab2e2b9e7a111d7c137f3e1e · TryGhost/Ghost
Independent technology for modern publishing, memberships, subscriptions and newsletters. - TryGhost/Ghost

Personal information

  • {first_name} - just the first name, extracted from the full name
  • {name} - full name
  • {email} - email address

Member status and subscription

  • {status} - free, paid, complimentary, or trialing
  • {status_text} - human-readable status with dates, like "Your subscription will renew on August 15, 2025"
  • {created_at} - when they became a member

Account management

  • {unsubscribe_url} - unsubscribe link
  • {manage_account_url} - link to the account portal
  • {list_unsubscribe} - the one-click unsubscribe URL Ghost puts in the List-Unsubscribe header. It's added to every email whether you reference it or not.

Identifiers and helpers

  • {uuid} - the member's UUID
  • {key} - a SHA-256 HMAC of that UUID, signed with your site's members validation key
  • {name_class} - CSS helper, returns "hidden" if no name is set
  • {uniqueid} - a fresh random UUID for every email, so image proxies can't cache one image across every recipient. Behind a private labs flag called emailUniqueid.

The member ID, and the version you need

%%{uuid}%% works in the email body from Ghost 6.17.0 onwards, released 5 February 2026. Before that, three lines in the renderer stripped it out before anything else ran. If you're on an older Ghost, none of the UUID advice below applies to you.

The commit that deleted those three lines is worth reading, because it explains the reversal:

we weren't previously clear on uuid being 'public' data, although we expose it in various places

That's the honest position. The UUID isn't a secret. Ghost already hands the same value to the comments widget in every reader's browser, a point Cathy made about this post months before Ghost's own code caught up.

The same change fixed the more annoying half of the problem. Ghost rewrites outbound links to run through its own redirect for click tracking, so a personalized link used to be a choice: keep the personalization or keep the stats. Now Ghost carries the placeholder through the rewrite and fills it in at redirect time, from the ?m= parameter on the tracking URL. You get both.

What still doesn't work

Four real limits.

The short form outside an email card

{first_name} in an HTML card stays literal text. Nothing wraps it, so the renderer never sees it. Type the %% yourself.

One exception you probably don't have: with the private emailUniqueid labs flag on, HTML cards run the same wrapping as email cards. That flag only appears once you've turned on developer experiments.

Image cards

An image you upload or paste becomes an image card with a fixed URL, and there's no field to put a replacement string into. Per-member images have to go in an HTML card.

Click tracking on anything but the UUID

Redirect-time substitution only knows how to fill in %%{uuid}%%. A link carrying %%{email}%% or %%{key}%% gets skipped by link tracking entirely and left to Mailgun. The link works, the personalization works, the click never reaches your analytics.

The web version of the post

An HTML card renders on your website as well as in the email, and on the website %%{uuid}%% is just text sitting on the page.

The fix is the card's visibility settings: turn off all three toggles under Web – Public visitors, Free members, Paid members – and the card only goes out by email.

That trap has a sharper version if you ever write about this. The renderer does not exempt code blocks, so every %%{uuid}%% in the examples on this page is a live replacement string as far as it's concerned. Send a post like this one as a newsletter and each subscriber gets their own UUID printed in the samples.

Which identifier to reach for

The UUID

The default choice now. Stable for the life of the member, and Ghost treats it as public.

Your personalized content:
https://example.com/content?member=%%{uuid}%%

The validation key

{key} is an HMAC of the UUID, signed with your site's members validation key. You can't reverse it, which is the point: it identifies a member to your server without the UUID passing through a Mailgun log or a referrer header.

Your personalized content:
https://example.com/content?key=%%{key}%%

The catch is that you have to compute the same HMAC for every member on your end before the key means anything.

The email address

Unique per member and always available, but readable by everything your link passes through, and it changes the moment someone updates their address. The UUID doesn't.

Your personalized content:
https://example.com/content?member=%%{email}%%&status=%%{status}%%&joined=%%{created_at}%%

Thirteen variables, substituted across the whole email, and one formatting rule that decides where they work. That's the entire system. The only genuinely hard part is that you have to read email-renderer.js to find any of it out.

Jannis Fedoruk-Betschki

Written by

Jannis Fedoruk-Betschki

Founder of Magic Pages. I build reliable Ghost hosting, so publishers can focus on what they do best − creating. When I'm not improving the platform, I'm probably helping a customer get their site just right.

You Might Also Like

Creating a custom 404 page for your Ghost site

Ghost's default 404 page is a dead end. It works, but it looks nothing like your site, and it gives the visitor almost nowhere to go. The good thing is, that you can easily replace it with a single file.

5 min read

WordPress to Ghost: is it worth it?

WordPress can do anything − and maintaining all of it is the job. An honest look at whether moving your blog to Ghost is worth it: what you gain, what you give up, and how to migrate without losing your SEO rankings.

7 min read

Websites powered by Magic Pages

From personal blogs to growing businesses — published with Ghost®, hosted with care.

Loading showcase sites...

Start Your 14-Day Free Trial

No credit card required