Render without sending

POST /api/v1/render Renders an email with sample data and returns the result. It sends nothing, stores nothing and changes nothing — it is the only endpoint here with no side effect, so call it as often as you like, before and after every edit. unsubscribeUrl, today, currentYear and poweredBy are not sample values: the server computes them and overrides anything you send for them. The date and the year come from the time zone and language set on the API key's organization, not from the caller and not from the server clock — so a render answers with the same date the recipient will read. One of templateId or content is required — sending neither answers 400. Sending both renders your content with the template's subject, which is how you try an edit before saving it.
json
{
  "success": true,
  "data": {
    "html": "<html>…<p>Hi Camille</p>…</html>",
    "text": "Hi Camille …",
    "subject": "What shipped in August",
    "unknownVariables": ["company"]
  }
}
unknownVariables lists the {{…}} placeholders that are not part of the vocabulary. They resolve to an empty string at send time — nothing breaks, but nothing shows either. Fix them here.
HTML without an unsubscribe link is refused, here and at send time. The answer is a 400 naming the problem: add {{unsubscribeUrl}} inside a link in your HTML. Nothing is ever appended to your markup on your behalf, so a template that renders is a template that can be sent.
The unsubscribe link used while rendering is a sample URL: a render has no recipient, so it has no real unsubscribe token. It is there to prove the link exists, not to work.

Authorization

Role in the organizationThis endpoint
OWNERAllowed
ADMINAllowed
MEMBERAllowed
A key carries the role its creator holds in this organization — never a role on AgentsMail itself. See API keys.

Request body

FieldTypeRequiredDescription
templateIdstringnoRenders a template of your organization
contentstringnoRenders raw HTML — wins over the template's own content
subjectstringnoDefaults to the template's defaultSubject
previewTextstring | nullnoUp to 200 characters after trimming. Rendered at {{previewText}} or {{preheader}}; absent, empty or null renders empty
preheaderstring | nullnoCompatible alias of previewText; different normalized values return 400
variablesobjectnoSample values: firstName, lastName, email
json
{
  "previewText": "Timely m’a bloqué mes paiements Stripe pendant 5 jours.",
  "content": "<div data-skip-in-text=\"true\" style=\"display:none;font-size:1px;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;mso-hide:all;\">{{previewText}}</div><p>Hello</p><a href=\"{{unsubscribeUrl}}\">Unsubscribe</a>"
}

Example

bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
  -d '{"templateId":"'$TEMPLATE_ID'","variables":{"firstName":"Camille"}}' \
  "https://www.agentsmail.io/api/v1/render"

Response

  • 200 OK — the rendered email.
  • 400 Bad Request — invalid fields, conflicting preview aliases, or required HTML missing.
  • 401 Unauthorized — missing or invalid key, or its owner left the organization.
  • 429 Too Many Requests — over 120 requests in a minute for this key.

Response fields

FieldTypeDescription
successbooleanIndicates if the operation was successful
data.htmlstringThe rendered document, merge tags resolved
data.textstringIts plain-text counterpart
data.subjectstringThe subject after resolution
data.unknownVariablesstring[]Tags the product does not know — left visible in the output
data.warningsstring[]missing_postal_address, dark_mode_contrast_corrected, dark_mode_contrast_unverified, dark_mode_declaration_removed, missing_preheader_shortcode

Example response

json
{
  "success": true,
  "data": {
    "html": "<html>…</html>",
    "text": "August news…",
    "subject": "August news",
    "unknownVariables": [],
    "warnings": []
  }
}

Agent recipe

Render is the only call with no side effect: loop on it until the document is clean, then create the campaign. An agent that skips this ships {{fistName}} to real inboxes.
js
const render = async (content) =>
  (
    await fetch(`${base}/render`, {
      method: 'POST',
      headers: {...headers, 'content-type': 'application/json'},
      body: JSON.stringify({content}),
    })
  ).json()

let html = draft
for (let attempt = 0; attempt < 3; attempt++) {
  const {data} = await render(html)
  if (data.unknownVariables.length === 0) break
  html = fixTypos(html, data.unknownVariables) // your own correction pass
}

Notes

previewText and preheader name the same value. If both are supplied, they must match after trimming (including null). A nonempty value without either shortcode outside HTML comments returns missing_preheader_shortcode. Nothing is inserted into the HTML.