10 min read

Personalization (Merge Tags)

Use merge tags like {{customer_name}} to send one survey to many recipients with personalized question text and analytics rollups.

Merge tags let you write one survey and serve it to many recipients with personalized question text. Instead of cloning a survey 20 times (once per customer), you write a question like *"How was your experience with {{customer_name}}?"*, upload a CSV with a `customer_name` column, and every recipient sees their own value.

You also get per-customer analytics rollups out of the box. The same five questions answered by 20 customers become a single dataset you can group by tag value.

Quick start

  1. Open a question in the builder.
  2. Click the `{ } Tag` button at the right edge of the title field.
  3. Pick a tag from the popover, or click *Create new tag…*
  4. Insert it: the title now reads *"How was your experience with \{\{customer_name\}\}?"*. The live preview shows it as a soft-blue pill.
  5. Open the Distribute tab → Personalization and upload a CSV with one row per recipient (`email,customer_name,…`). Each row's values are frozen onto that recipient's invite.
  6. Send invites and launch. Each recipient sees their personalized question. Analytics show per-customer rollups; the CSV export gets one column per tag (`mc_customer_name`).

What you can use tags for

Tags work in question titles and question descriptions. They also work in invitation email subject + body when you send invites via the API.

Limitation today: Multiple-choice / checkbox / dropdown / matrix / ranking option labels are not yet personalized in the respondent UI. If you put `\{\{customer_name\}\}` in an option label, recipients will see the raw `\{\{customer_name\}\}` text. The builder preview renders pills correctly, so it's easy to spot and avoid. Personalized option labels are tracked as a follow-up.

Tag types

There are three kinds of tags:

System tags (always available)

Three tags are auto-seeded on every survey. You can use them immediately without declaring anything:

  • `email`: recipient's email address
  • `first_name`: recipient's first name
  • `last_name`: recipient's last name

System tags cannot be renamed or deleted.

Custom tags (per-survey)

Anything you create from the *Create new tag…* form. Use these for things specific to one survey:

  • `{{customer_name}}`
  • `{{region}}`
  • `{{tier}}`
  • `{{trip_destination}}`

Library tags (org-wide)

When creating a custom tag, check *"Save to organization library"* and it becomes reusable across every survey in your organization. New surveys can import it from the *From library* section of the picker.

Tag rules

  • Keys: lowercase letters, numbers, and underscores only. Must start with a letter. 1–40 characters. Examples: `customer_name` ✓, `Customer-Name` ✗, `1tier` ✗.
  • Values: capped at 200 characters. Longer values are truncated server-side with an ellipsis.
  • HTML safety: every value is HTML-escaped at render time. You cannot inject markup or scripts via a tag value.
  • No recursion: if `{{a}}` resolves to the literal text `{{b}}`, recipients see `{{b}}`; values are not re-interpolated.

Required flag and fallbacks

When you create a tag, two optional knobs control behavior when a recipient has no value:

  • Required: marks the tag as important to the survey design. Today this is advisory: the survey can still launch even if some recipients are missing the value. The Distribute tab shows a soft-warning banner so you can fix the data or accept the blanks.
  • Fallback: a default string used when the recipient has no value (e.g. fallback `"valued customer"` makes the title read *"How was your experience with valued customer?"*). If a tag has no fallback and the recipient has no value, the placeholder renders as an empty string.

Public surveys (anyone with the link can take it) must declare a fallback for every required tag referenced in question text. Anonymous traffic has no recipient identity, so without a fallback they would see blanks. The launch button blocks until you set the fallbacks.

Distributing personalized surveys

There are three ways to give a recipient a tag value:

Open Distribute → Personalization (the section appears only when the survey has at least one custom tag).

  1. Upload a CSV with one row per recipient. Required column: `email`.
  2. The mapping panel auto-maps system tag aliases (`Email`, `E-Mail`, `Email Address` all match `email`) and exact-match custom tag keys (your column `customer_name` matches the `customer_name` tag).
  3. Override any mapping with the column dropdown.
  4. Click Create N invitations. Each row becomes a SurveyInvite with that row's values frozen onto it.

The success card lists each invite URL so you can copy them or distribute them however you like (email, Slack, Notion table, etc.).

2. API invitation (for partners and integrations)

When you POST to `/api/v1/surveys/{id}/invitations` with an `mergeData` field, every invite in the batch shares those values. Useful when one batch is for the same customer:

POST /api/v1/surveys/{id}/invitations
{
  "targeting": { "type": "all_active" },
  "mergeData": { "customer_name": "Acme", "region": "EU" }
}

Share links carry merge data inside an HMAC-signed query string. Generate one via:

POST /api/v1/surveys/{id}/share-links
{ "mergeData": { "customer_name": "Acme" } }

You get back a URL with `?ctx=…&sig=…&v=1`. The signature prevents anyone from changing values by editing the URL. Default TTL: 30 days.

Anonymous URL params (e.g., `?customer_name=Foo`) are rejected. This is intentional: letting anyone pass arbitrary tag values would let them impersonate other customers in the survey.

What recipients see

The first time a recipient opens an invite link or signed share link, the merge values are resolved once and frozen onto a server-side session. Subsequent loads (page refresh, navigating between questions) read from the frozen session; URL parameters are ignored after that.

Resolution priority for each tag:

  1. Invite `mergeData` (CSV row, API invitation body)
  2. Contact properties (when the invite has an associated contact in your contact list)
  3. Respondent attributes (HRIS-synced data)
  4. Signed-link payload (when arriving via a signed share link)
  5. Tag fallback
  6. Empty string

Saving and launching

  • Save-time: if any question text references an undeclared tag (e.g., you typed `{{custmoer_name}}` instead of `{{customer_name}}`), the save is rejected with a clear error. Declare the tag first or fix the typo.
  • Launch-time: the survey is blocked from launching only for things that would actually break:
    • Undeclared tags in question text (`UNDECLARED_TAG`)
    • Required tags without a fallback on a public-link survey (`NO_FALLBACK_FOR_PUBLIC_LINK`)
  • Per-recipient gaps are NOT a launch blocker. If 3 of 1000 invites are missing a value for a required tag, the survey can still go live. Those 3 recipients see the fallback (or blank if no fallback). The Distribute tab shows an amber banner: *"3 invites missing required personalization values"* so you can fix the rows if you want.

When launch is blocked, a modal appears listing every blocker with deep-links to the relevant builder area. Resolve the issues, then click Launch again.

Renaming a tag

Open *Manage tags…* from the bottom of the tag picker, click *Edit* on the tag, expand *Rename key*, and confirm the rewrite. Renaming is a single-transaction cascade that rewrites every `\{\{old_key\}\}` → `\{\{new_key\}\}` reference in the survey's title, description, welcome/completion messages, and every question's title, description, and option values. The result toast tells you how many questions and options were updated.

System tags (`email`, `first_name`, `last_name`) cannot be renamed.

Deleting a tag

Click *Edit → trash icon* on a tag in *Manage tags*. If the tag is still referenced anywhere in the survey, the delete is rejected and the panel lists the locations (e.g. *survey.title*, *question.title · q-abc*, *question.option · opt-xyz*) so you can clean them up first.

System tags cannot be deleted.

Existing responses keep their snapshotted values when a tag is deleted, so analytics for past responses still work. Future invites just won't resolve the tag.

Analytics and exports

Once recipients submit responses with merge values, two new things become available:

  • "Group by Personalization Tag" panel appears in the analytics dashboard below the demographics filter. It lists every tag that's been seen on a response, with per-value response counts and percentages, the per-customer rollup the feature was built for.
  • CSV export gains one column per declared tag, prefixed `mc_` (e.g. `mc_customer_name`, `mc_region`). Each row's column is filled from that response's frozen merge context.

Limitations

We're transparent about what doesn't work yet:

  • Option labels are not yet personalized in the respondent UI. Title and description work; multiple-choice / checkbox / dropdown / matrix / ranking option labels render the raw `\{\{tag\}\}` text. Use tags in titles and descriptions for now.
  • Smart Surveys (AI-powered conversational surveys) are not yet supported. Their submission path doesn't snapshot the merge context. Standard surveys are fully supported.
  • No conditional branching on tag values. If you need *"if customer_name = 'Acme', show question X"*, use the survey's built-in skip logic on a real question instead.
  • No per-locale tag value translation. Values are stored as-is.
  • Tags are not written back to Contact records. When the resolver pulls a value from `Contact.properties`, it's read-only; Revuloop never updates the contact.
  • AI analytics pipeline doesn't filter by tag yet. The merge context is preserved on each response, so this is data-preserving; the AI summary surface just doesn't know about it.
  • Tag library deletes don't cascade into surveys. Surveys that imported a library tag keep their cloned copy when the library entry is removed.

Security

  • Every value is HTML-escaped before being inserted into rendered output (titles, descriptions, email HTML).
  • Anonymous URL parameters are rejected. Only signed share links can carry merge data outside an invitation flow.
  • Signed links use HMAC-SHA256 with a server-side secret (`SHARE_LINK_SECRET`). Tampering invalidates the signature and the link is rejected.
  • Sessions are frozen. Once a recipient opens a link, the resolved merge context is saved to a `ResponseSession` row. URL params on subsequent loads are ignored, so there's no way to overwrite the recipient's identity mid-survey.

Troubleshooting

"Why am I seeing `\{\{customer_name\}\}` literally in my survey preview?"

The tag isn't declared on the survey. Click the `{ } Tag` button on the question and either select an existing tag with that key or create a new one. Saves are rejected when there are undeclared references; the error message names the bad key.

"My CSV column `Customer Name` didn't map automatically."

Custom tags require an exact normalized match between the column header and the tag key. `Customer Name` normalizes to `customer_name`, which would match a tag with key `customer_name` exactly. `CustomerName` (no space) normalizes to `customername` and won't match `customer_name`. Override the mapping manually in the column dropdown if needed. System tags use a more permissive alias list (`Email`, `E-Mail`, `Email Address`, etc.).

"My Required tag is missing on 3 of 1000 invites. Why isn't the launch blocked?"

Per-recipient data quality is intentionally a soft warning. The Distribute tab shows a banner with the count; the survey can still launch. Those 3 recipients will see the tag's fallback (or blank if no fallback). To enforce strict per-recipient values, set a fallback on the tag.

"Public link surveys won't launch. What does `NO_FALLBACK_FOR_PUBLIC_LINK` mean?"

Anonymous traffic has no recipient identity, so any required tag referenced in the survey must have a fallback that anonymous visitors will see. Edit the tag in *Manage tags* and set a fallback like `"valued visitor"`.

Still need help?

Our support team typically responds within 24 hours