---
title: "Render sequence step HTML"
method: POST
path: "/sequences/{sequenceId}/nodes/{nodeId}/render"
tags: ["Sequences"]
---

# Render sequence step HTML

`POST /sequences/{sequenceId}/nodes/{nodeId}/render`

Render one sequence email step to the exact email-safe HTML that would be sent, for embedding a visual preview. Read-only: this never sends or modifies anything, and uses POST only so personalization input can travel in a request body.

## Path parameters

- `sequenceId` string, required
- `nodeId` string, required

## Request body

- RenderEmailRequest — Personalization and output options for a render. Omit every field to render for a sample contact with clean links.
  - `subscriberId` string — Personalize as this stored subscriber. Mutually exclusive with subscriber. The rendered HTML then carries that subscriber's details, so this field additionally requires the subscribers:read scope.
  - `subscriber` object — Personalize as an ad-hoc contact. Mutually exclusive with subscriberId.
    - `email` string, email, required
    - `firstName` string, nullable
    - `lastName` string, nullable
    - `customAttributes` object
    - `tags` string[] — Tags this ad-hoc contact carries, used to evaluate tag block conditions. Nothing is stored. Without them a tag condition has no tags to read and renders as false, so only the else branch of a tag split can be previewed. Every other stored-state condition still needs subscriberId.
  - `variables` object — Extra merge variables layered over the contact's attributes.
  - `locale` string — Force a localization locale instead of deriving it from the contact.
  - `variantId` string — Render a specific A/B test variant. Ignored for templates.
  - `tracking` boolean — Apply the company's auto-UTM link decoration as a real send would. Per-send click redirects and the open pixel need a real email send record and are never present in a render.

## Response `200`

Email rendered successfully

- RenderEmailResponse
  - `success` boolean, required
  - `html` string, required — Email-safe HTML document, rendered exactly as it would be sent.
  - `subject` string, required — Subject line with merge tags resolved.
  - `previewText` string, nullable, required — Inbox preview text with merge tags resolved.
  - `locale` string, required — Localization locale the render resolved to.
  - `personalized` boolean, required — False means no contact was supplied, so a sample contact was used and contact-specific merge tags resolved to empty values.
  - `trackingApplied` boolean, required — Whether auto-UTM link decoration was applied.
  - `unresolvedMergeTags` object[], required — Merge tags that rendered as an empty string. An unrecognized tag and a recognized but blank one are identical in the HTML, so this is the only way to tell them apart. Empty when every tag resolved.
    - `tag` string, required — Tag name as authored, without braces.
    - `reason` 'unknown' | 'no_value', required — unknown - nothing provides this name, so it stays empty for every recipient. no_value - recognized or unverifiable, but blank for this contact. An unknown name is reported even when a default filter supplied text in its place, because that fallback then reaches every recipient while the rendered HTML looks correctly personalized; a recognized name that is merely blank for this contact is not reported when it has a default. A name is only called unknown when the render had a source to check it against. Without the contact's attributes nothing is checkable, since a bare {{plan}} reads the same attribute map as {{subscriber.plan}}, so pass a stored subscriberId or an inline subscriber with customAttributes. Beyond that, event.* needs sample event properties in variables, recommendedProducts.* needs a stored subscriberId the catalog has something to recommend for, and discount.* is only checkable on a sequence step whose incoming paths all run the same discount step. Rendering a transactional email is checkable only when variables is passed, since its tags come from the variables of each send call and carry no prefix marking them. Otherwise those tags land in no_value rather than in unknown. An optional attribute this contact never had set is kept out of unknown by checking the names other contacts in the account carry, which needs the subscribers:read scope; a key without it may report such a name as unknown.
  - `unevaluatedConditions` object[], required — Block conditions this render could not decide. Each was rendered as false, the same fail-closed rule a live send uses, so an else branch in the HTML is not evidence that the condition is false for a real recipient. Empty when every condition was actually evaluated.
    - `field` string, required — Condition field, such as tag or segment.
    - `operator` string, required
    - `value` string, required
    - `description` string, required — The condition in the words the dashboard uses.
    - `reason` 'requires_stored_subscriber' | 'invalid_filter' | 'evaluation_failed', required — requires_stored_subscriber - the field reads stored subscriber state, so pass subscriberId, or for a tag condition pass tags on the inline subscriber. invalid_filter - the stored condition is malformed, which fails closed on a real send too; hint carries the validation error. evaluation_failed - the lookup itself failed and the render is worth retrying.
    - `hint` string, required — What to change to get this condition evaluated.
  - `entity` object, required
    - `type` 'campaign' | 'sequence_step' | 'template', required
    - `id` string, required
    - `variantId` string, nullable, required

## Other responses

- `400` — Validation error
- `401` — Unauthorized
- `403` — API key is missing the required read scope, or is missing subscribers:read while subscriberId was supplied
- `404` — Sequence or step not found

## Changes

- **2026-08-23** `1a40912815df` — 2 info
  - added the new optional request property `subscriber/tags`
  - added the required property `unevaluatedConditions` to the response with the `200` status
- **2026-08-19** `ba3a29c6eb33` — 4 info
  - added the optional property `retryable` to the response with the `400` status
  - added the optional property `retryable` to the response with the `401` status
  - added the optional property `retryable` to the response with the `403` status
  - added the optional property `retryable` to the response with the `404` status
- **2026-08-16** `6fd2903b91ce` — 4 info
  - added the optional property `code` to the response with the `400` status
  - added the optional property `code` to the response with the `401` status
  - added the optional property `code` to the response with the `403` status
  - added the optional property `code` to the response with the `404` status
- **2026-08-02** `36f92fc0caa2` — 4 breaking
  - the response property `success` became optional for the status `400`
  - the response property `success` became optional for the status `401`
  - the response property `success` became optional for the status `403`
  - the response property `success` became optional for the status `404`
- **2026-07-29** `d03612b5fabd` — 1 info
  - added the required property `unresolvedMergeTags` to the response with the `200` status

[Full history](https://skmtc.dev/sequenzy/apis/sequenzy-api/changes/sequences/:sequenceId/nodes/:nodeId/render/post.md)

---

[API](https://skmtc.dev/sequenzy/apis/sequenzy-api.md) · [All operations](https://skmtc.dev/sequenzy/apis/sequenzy-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/sequenzy/sequenzy-api/revisions/73f826277d9a/schema)
