Back to developer portal

Personalization (Merge Tags)

Embed placeholders like {{customer_name}} in question text. Each recipient sees their own resolved value while a single Survey + question_id keeps analytics aggregating cleanly across the whole audience.

Overview

Tags are first-class objects on a survey. Question text containing {{undeclared_tag}} is a hard error at save time. You declare a tag once, then reference it freely. At runtime, the resolved values are frozen onto a server-side ResponseSession on the recipient’s first survey load and cannot be changed by tampering with URL parameters.

Per-survey + library

Three tag sources: SYSTEM, CUSTOM, FROM_LIBRARY. Library entries are cloned, not referenced live.

HTML-escaped

Every interpolated value is HTML-escaped server-side; XSS via merge values is not possible.

Frozen at session start

Anonymous URL params are rejected. Only invitation tokens or HMAC-signed share links carry merge data.

Tag rules

ConstraintValue
Key format^[a-z][a-z0-9_]{0,39}$ . Lowercase snake_case, 1–40 chars, must start with a letter
Value cap200 characters. Longer values are truncated server-side with an ellipsis.
Placeholder syntax{{ key }} , surrounding whitespace allowed. Uppercase or invalid keys are not matched.
System tags (auto-seeded)email, first_name, last_name . Cannot be renamed or deleted
Recursive interpolationDisabled. A value containing {{other}} renders as literal text.
Session TTL7 days, sliding on activity. Same (surveyId, inviteToken) resumes the same session.
Signed share-link TTL30 days default; 60s minimum, 1y maximum

Endpoints

All endpoints under /api/v1/... require an organization API key (rlk_live_* / rlk_test_*) with the scopes listed below. See Authentication for details.

GET / POST/api/v1/surveys/{id}/merge-tags
Required scopes: surveys:read / surveys:write

List declared tags or create a new CUSTOM tag. POST body: { key, label, required, fallback?, saveToLibrary? }.

GET / PATCH / DELETE/api/v1/surveys/{id}/merge-tags/{tagId}
Required scopes: surveys:read / surveys:write

Read, update, or delete a tag. PATCH supports rename-with-cascade. Pass renameKey=true and a new key to rewrite every reference in the survey atomically. DELETE returns 409 STATE_CONFLICT with location details when the tag is still referenced.

POST/api/v1/surveys/{id}/merge-tags/import
Required scopes: surveys:write

Clone an OrgMergeTag into the survey with source=FROM_LIBRARY. Body: { libraryTagId, required?, fallback? }.

GET / POST/api/v1/organization/merge-tags
Required scopes: organization:read / organization:write

List or create org-wide library tags. Library entries are templates. They are cloned into surveys via the import endpoint, not referenced live.

POST/api/v1/surveys/{id}/share-links
Required scopes: surveys:write

Generate an HMAC-signed share URL carrying merge data. Returns { url, expiresAt, ctx, sig, v }. Requires SHARE_LINK_SECRET env var; rejects mergeData keys that are not declared on the survey.

POST/api/v1/surveys/{id}/sessions
Required scopes: surveys:write

Materialize a ResponseSession for a respondent. Body: { inviteToken? | signedCtx + signedSig + signedV? }. Idempotent on (surveyId, inviteToken).

POST/api/v1/surveys/{id}/invitations
Required scopes: surveys:write, respondents:read

Existing invitations endpoint now accepts an optional mergeData field. The same values apply to every invite in the batch and are persisted to SurveyInvite.mergeData.

Render path

When a recipient opens an invite or signed share link, the page calls ensureResponseSession server-side. The function resolves merge values once, persists them to a ResponseSession row, and passes them to the client alongside the declared tags.

Resolution priority (per declared tag)

1. SurveyInvite.mergeData       (CSV row, batch invitation API)
2. Contact.properties           (when invite has an associated contact)
3. Respondent.attributes        (HRIS-synced data)
4. Signed-link payload          (when arriving via signed share URL)
5. Tag fallback
6. ""                           (empty string)

The resolved context is HTML-escaped before injection into rendered question titles, descriptions, and email HTML. Anonymous URL parameters (e.g. ?customer_name=Foo) are not honored, only invitation tokens and signed share links can carry merge data.

Submission snapshot

When a recipient submits, the resolved context is snapshotted onto Response.sourceDetails.mergeContext alongside a contextSource tag (INVITE / CONTACT / SIGNED_LINK / ANONYMOUS). The originating ResponseSession is marked completedAt and linked to the new response.

Response.sourceDetails example

{
  "browser": "Chrome",
  "device": "desktop",
  // ...existing fields
  "mergeContext": {
    "customer_name": "Acme",
    "region": "EU"
  },
  "contextSource": "INVITE"
}

Launch validation

POST /api/v1/surveys/{id}/launch evaluates merge-tag readiness before transitioning the survey to ACTIVE. Two blockers can produce 409 STATE_CONFLICT:

UNDECLARED_TAG

Question text references a tag that is not declared. Save-time validation also rejects this; it can only reach launch if the data was inserted out-of-band.

NO_FALLBACK_FOR_PUBLIC_LINK

The survey has a public link enabled and at least one required tag has no fallback. Anonymous traffic would see blanks; set a fallback or remove the public link.

Per-recipient data quality is intentionally not a launch blocker. If some pending invites are missing a required tag value, the survey can still launch. Those recipients see the fallback (or blank). The Distribute tab shows a soft-warning banner with the missing-invite count instead.

Current limitations

  • Question option labels (multiple-choice, checkbox, dropdown, matrix, ranking) are not yet interpolated in the respondent UI. Use tags in question titles and descriptions. Builder preview renders pills correctly so it’s easy to spot.
  • Smart Surveys (AI-powered conversational surveys) do not currently snapshot the merge context onto their responses. Standard surveys are fully supported.
  • Conditional logic on tag values is not supported. Use the survey’s built-in skip logic on a real question instead.
  • Per-locale tag value translation is not supported. Values are stored as-is.
  • Read-only contact integration. The resolver pulls values from Contact.properties, but Revuloop never writes back to the contact record.
  • AI analytics pipeline does not currently filter or group by merge tag. The merge context is preserved on each response, so this is data-preserving. Only the AI-summary surface is unaware.
  • Library tag deletes do not cascade into surveys that imported them. The cloned SurveyMergeTag continues to function with libraryTagId nulled out.

Webhook implications

Existing webhook events (RESPONSE_SUBMITTED, etc.) deliver the full Response object. The sourceDetails.mergeContext snapshot is included automatically, no new event types and no opt-in required. Subscribers who currently parse sourceDetails.demographics can extend their handlers to read sourceDetails.mergeContext using the same pattern.

Environment

VariablePurpose
SHARE_LINK_SECRETHMAC secret for share-link signing/verification. Generate with openssl rand -hex 32. 32+ characters required.