---
title: Text, native HTML and Zapier
description: Write normally or bring your own HTML. One body contract for variants and messages.
---

The same fields work in campaign variants, standalone messages and inline sent previews.

| Input | Result |
| --- | --- |
| `text` only | Escaped HTML with line breaks, plus the original plain-text alternative. |
| `html` only | Your authored layout, colors, tables, images and CSS, with resolved variables. |
| Both | Your HTML and your separate plain-text alternative. |

Supply at least one non-null, nonempty body. Empty supplied strings are rejected, including when the other body exists. No automatic HTML detection, Markdown parsing, AI rewriting, brand theme or template resource is involved. With `prompt: null`, authored content remains deterministic. A generation prompt explicitly opts into AI.

## Write normally

For a campaign variant:

```json
{
  "name": "Short introduction",
  "subject": "A quick question",
  "text": "Hello {{ recipient.given_name | default(\"team\") }},\n\nWould you like to see an example?\n\n{{ footer }}",
  "prompt": null
}
```

Newlines become HTML breaks. Literal `<`, `>` and `&` are escaped, so writing `<strong>hello</strong>` in `text` displays those characters rather than adding bold. MiniJinja variables and conditions work in both formats. Supplied values are data, never executable template code. Put actual markup in `html` when you want a design.

The API converts text once when accepting content. Campaign reads expose the normalized `html` alongside `text`; the existing queue and renderer handle delivery. When replacing a variant with new text-only content, omit `html` or set it to null. Sending an old HTML value alongside new text intentionally preserves that HTML. Each source and rendered body is limited to 100,000 characters, including generated markup and an inherited signature. Escaping can expand text past that limit.

## Bring your own HTML

```json
{
  "name": "Our own design",
  "subject": "Hello {{ recipient.given_name | default(\"team\") }}",
  "html": "<!doctype html><html><head><style>.brand { color: #18332f; }</style></head><body><table role=\"presentation\" width=\"100%\"><tr><td style=\"background:#fff;color:#111;padding:24px\"><h1 class=\"brand\">Hello {{ recipient.given_name | default(\"team\") }}</h1><p>Your own content.</p>{{ footer }}</td></tr></table></body></html>",
  "prompt": null
}
```

Your styles and markup remain the source; Norbelys does not regenerate or recolor them. Only the supported personalization syntax is evaluated. Inserted values are HTML-escaped. Do not use untrusted values as entire HTML fragments, CSS rules or unchecked URLs. Email clients decide which CSS and images they display; custom HTML does not override their rendering limitations. Use [images](/email-images) for stable image URLs.

Configured features still apply: the optional preheader, sender signature, campaign open/click tracking and campaign unsubscribe handling. Use `{{ footer }}` to position the optional sender signature inside your layout; without a slot it appends before `</body>` where available. A sender without a footer adds none. Standalone messages and previews do not gain campaign engagement tracking. See [signatures](/sender-profiles).

## A small Zapier mapping

Use an HTTP POST action with a JSON body, such as [API by Zapier](https://help.zapier.com/hc/en-us/articles/44391650357005-Send-API-requests-in-Zap-workflows). For Webhooks by Zapier, choose [Custom Request](https://help.zapier.com/hc/en-us/articles/8496326446989-Send-webhooks-in-Zap-workflows) with method POST to preserve the nested JSON shape:

```json
{
  "from": "connected-sender@example.com",
  "to": ["recipient@example.com"],
  "subject": "A quick question",
  "text": "Hello {{ variables.first_name }},\n\nWould you like an example?",
  "variables": { "first_name": "Ada" }
}
```

Send it to `https://api.norbelys.com/v1/messages`, with `Authorization: Bearer YOUR_KEY`, `Content-Type: application/json`, and an `Idempotency-Key` derived from the source event. Map the upstream person's name into `variables.first_name` rather than assembling template source from their data. Map the body to `text`, or to `html` for customer HTML; omit unused body fields rather than sending empty strings. Never send a new idempotency key just because the response was lost. `202` means queued; use the response ID to check delivery through the same API. Authentication, pacing and suppression stay on the server.

This is an HTTP mapping, not a claim that a separately published Zapier app exists. No preprocessing code step or external template engine is required.
