Skip to main content
POST
Render Sequence Step HTML
Render a single sequence email step to the exact email-safe HTML that would be sent. Use this to show a visual preview of a step inside your own dashboard or email builder. The render applies everything the send path applies: your email theme and font, company branding and footer, localization, block visibility conditions, product recommendation blocks, and merge tags.
This endpoint is read-only. It requires the sequences:read scope and never sends or enrolls anyone. It uses POST only so personalization input can travel in a request body.

Request

string
required
Sequence ID.
string
required
Email step node ID. Get it from Get Sequence.
string
Personalize as this stored subscriber. Use this or subscriber, not both. Because the rendered HTML then carries that subscriber’s details, this field also requires the subscribers:read scope.
object
Personalize as an ad-hoc contact that does not need to exist in your account. Accepts email (required), firstName, lastName, customAttributes, and tags. Use this or subscriberId, not both.
string[]
Tags this ad-hoc contact carries, used to evaluate tag block conditions. Nothing is stored. Without it a tag condition has no tags to read, so only the else branch of a tag split can be previewed.
object
Extra merge variables layered over the contact’s attributes.
string
Force a localization locale instead of deriving it from the contact.
string
Render a specific A/B test variant. Required when the step is an A/B test (action_ab_test) step: those steps have no email of their own, and copy lives on the variants from Get Sequence. Sequence variants also need the ab_tests:read scope, the same scope Get Sequence uses to return competing variant copy.
boolean
default:"false"
Apply your company’s auto-UTM link decoration, as a real send would. Defaults to false so the preview shows your own clean URLs.

Discount codes

When the step sits downstream of a create-discount step, {{discount.code}} and the other {{discount.*}} tags resolve against that step’s real configuration - percentage or amount, currency, expiry - with a placeholder TEST-CODE in place of the code itself. Real codes are minted per enrollment, so a preview has none to show, and this lets you check the copy before anyone is enrolled. If the step can be reached by paths that do not all run the same discount step, the preview leaves the tags empty. A real send fills them from whatever discount that recipient’s own run created, so recipients who took a path through the discount step still get a code - the preview has no way to know which path a recipient took, so it declines to guess rather than showing a code the email may not have.

Checking merge tags

An email never shows a raw {{TAG}} to a recipient, so a tag that does not exist and a tag that is simply blank for this contact both render as an empty string. unresolvedMergeTags tells them apart:
  • "reason": "unknown" - nothing provides that name, so it will be empty for every recipient. Usually a typo, or a tag carried over from another platform.
  • "reason": "no_value" - the name is recognized but has no value for this contact.
A tag with a fallback is reported too when its name is unrecognized. {{ subscriber.frstName | default: "there" }} renders there for everybody, including the recipients whose first name is stored, and the HTML looks correctly personalized - so an empty unresolvedMergeTags array, not the rendered greeting, is what tells you personalization works. A recognized name that is merely blank for this contact is not reported when it has a fallback, since that is exactly what a fallback is for. A name is only called unknown when the render had a source to check it against. Without the contact’s attributes nothing is checkable at all - a bare {{plan}} reads the same attribute map as {{subscriber.plan}} - so pass a stored subscriberId, or an inline subscriber that includes customAttributes. Beyond that, {{event.*}} needs sample event properties in variables, since a real send fills those from the enrolling event, and {{recommendedProducts.*}} needs a stored subscriberId the catalog has something to recommend for. Without those, the tags come back as no_value rather than being reported as typos. An optional attribute that this contact happens not to have set is kept out of unknown by checking the names other contacts in the account carry, which is subscriber data: that check needs the subscribers:read scope, so a key without it may report such a name as unknown. {{discount.*}} is checkable only when every path into the step runs the same discount step. An empty array means every tag in the email resolved.

Checking conditions

Conditional blocks split on subscriber data, and the fields that read stored subscriber state - tag, segment, list, status, event, purchases, and engagement - are evaluated per recipient at send time. A render can evaluate them for a stored subscriberId, and it can evaluate a tag condition for an inline contact that lists its own tags. Anything else it cannot check. A condition it cannot check renders as false, the same fail-closed rule a live send uses, so the else branch appears in the HTML exactly as it would for a contact who genuinely does not match. unevaluatedConditions is what tells those apart:
  • "reason": "requires_stored_subscriber" - the field reads stored subscriber state. Pass subscriberId, or for a tag condition pass subscriber.tags.
  • "reason": "invalid_filter" - the stored condition is malformed, so a real send fails it closed too. hint carries the validation error.
  • "reason": "evaluation_failed" - the lookup itself failed. The render is worth retrying.
An empty array means every condition in the email was actually evaluated, so the branches in the HTML are the branches this contact would receive.

What is not in the HTML

Per-send click-redirect wrapping, the open tracking pixel, and unsubscribe click tracking are applied at send time against a real email send record, so they are never present in the returned HTML. The tracking flag controls auto-UTM decoration only. Without a subscriber the footer unsubscribe link points at a placeholder URL rather than a working token.

Responses