Skip to main content
AhaSend Markup, AhaML for short, is a text form of the designs that the Simple Editor makes. You read a design as AhaML and write one back as AhaML through the API, in content.markup, without the editor’s own JSON. AhaML is XML. Each tag is one part of the design, such as a paragraph, a button or a bold word, and each attribute is one of its settings.

How reads and writes work

The design is stored in the editor’s own form. A read prints it as AhaML, and a write turns the AhaML back into that form, so the editor opens a design written as AhaML as it opens any other.
  • A read prints one fixed form: the same design always gives the same AhaML, byte for byte.
  • AhaML that a read gave, sent back unchanged, keeps the stored design as it is.
  • A write sets the whole design. What the AhaML does not give takes its default; nothing of the stored design is kept.
  • A write checks the whole AhaML and refuses it with the problems it finds, up to the first 100, each with its line, column and path when it has a place in the AhaML. A syntax error, and a few other problems, stop the check and come back alone (see Errors).
  • A write is checked by the rules of this page also when it sends back a design it read: a design made in the editor that breaks a rule, such as one with an image on http:, must be fixed before it can be written as AhaML.

The document

  • Email is the root. It takes version and dir.
  • Theme is optional. When it is there, it is the first child of Email, and there is at most one. A write without Theme gives the default theme, as an empty <Theme/> does. A read prints no Theme when every setting is at its default.
  • The other children of Email are blocks.
  • Tag names are case-sensitive.

Versions

<Email version="1"> names the version of AhaML. A write may leave version out, which means the current version. A read always prints it. A new tag, attribute or value is added to the current version. Only a change that breaks AhaML that works today makes a new version. When a new version comes, a write in the old version still works.

Tags

“Blocks” are Paragraph, Heading, BulletList, OrderedList, Quote, Divider, Button, Image, Spacer, Section, Columns, Repeat and Footer. They, ListItem and Column each stand on their own line in the AhaML a read prints. “Text and inline tags” are text, Var, InlineImage, Br and the mark tags: Link, Color, Highlight, Bold, Italic, Underline, Strike and Code. A Repeat may not hold another Repeat, at any depth. A tag that must hold content refuses to be empty. Any other tag may be empty, as an empty paragraph is: <Paragraph/>.

Attributes

An attribute that is not given takes its default. A read leaves out each attribute that is at its default, apart from a required one, which it always prints: <Heading level="1"> keeps its level. “Not set” means that the tag gives no value of its own: the theme’s value applies where the theme has one, as for a font or a button color, and otherwise there is none, as for an alt or a fallback. The dir of a block is ltr, rtl or auto, and its default is the dir of Email. So a right-to-left design gives dir="rtl" on Email, and a read prints dir on another tag only when its direction differs.

Email

Paragraph

Paragraph also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Heading

Heading also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

BulletList

BulletList also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

OrderedList

OrderedList also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

ListItem

Quote

Divider

Divider also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Button

Button also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Image

Image also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Spacer

Spacer also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Section

Section also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Columns

Columns also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Column

Repeat

Repeat also takes the rule attributes showIf, hideIf, operator and value (see Show-if and hide-if).

Var

InlineImage

Br

Color

Highlight

Bold

Bold takes no attributes.

Italic

Italic takes no attributes.

Underline

Underline takes no attributes.

Strike

Strike takes no attributes.

Code

Code takes no attributes.

The theme

Theme sets the look of the whole email. A setting it does not give takes its default. fontFamily, fontFallback, webFontUrl and webFontFormat are one group:
  • A Theme with none of them has the default font.
  • A Theme with any of them needs fontFamily and fontFallback, and webFontUrl and webFontFormat come together or not at all.
  • A font with no web font file uses the font installed on the reader’s device, or the fallback.

Text and whitespace

Text follows the whitespace rules of HTML:
  • Each run of spaces, tabs and newlines is one space, also across the edge of a mark tag.
  • Spaces at the start and the end of a block’s text are removed, and so are spaces just before and just after a <Br/>.
  • <Br/> is the only line break.
So you can indent AhaML as you like. Whitespace between blocks is ignored, and other text where only blocks may go is refused. Marks can nest in any order, and a read prints them nested in one order: Link, Color, Highlight, Bold, Italic, Underline, Strike, Code, from outer to inner. A mark may not stand inside the same mark, such as Bold inside Bold. Code takes no other mark. A mark tag with no text adds nothing.
Marks around a Var, an InlineImage or a <Br/> are kept in the design, as the editor keeps them, but the email draws the value, the image or the line break without them. To make a value bold or a link, mark the text around it. To link an inline image, give it externalLink.

Variables

<Var name="first_name" fallback="there"/> is the only way to write a variable in text. It is filled in when the email is sent, and shows its fallback when the variable has no value. {{ name }} written as plain text stays plain text, and a write gives the warning jinja_in_text. The editor shows it as text. It is still filled in when the email is sent, unless it holds a character that HTML escapes (', ", <, > or &), which breaks it. These are refused as text_not_allowed_here, because they would break the email when it is sent:
  • text that ends with { just before a Var. Put a space between them.
  • a whole {{ }} or {% %} pair across a change of marks, an InlineImage or a <Br/>.
  • a {{ or {% that is not closed in its own text, before the end of its block and before the next Var.
A fallback may not hold {{, {% or {#, and may not end with {. URLs take variables as {{ name }} pieces, as in https://example.com/orders/{{ order_id }} (see URLs).

Show-if and hide-if

The blocks that take a rule show or hide by the value of a variable when the email is sent:
  • A tag takes showIf or hideIf, not both.
  • value is required for every operator except is_true and is_false, and refused for those two. An empty value="" is a value.
  • For greater_than and less_than, value is a number, such as 5 or 1.5. An empty value is 0.
  • operator or value without showIf or hideIf is refused.

Repeated blocks

<Repeat each="items"> repeats its blocks once for each item of the list items. Inside a Repeat, a name such as <Var name="title"/> reads the item’s title first, and the top-level title when the item has none. In a list of plain values, not objects, <Var name="value"/> is the item.

Values

Names

Var name, showIf, hideIf, each and the names in URL pieces start with an ASCII letter (a to z, A to Z) or _, and hold only ASCII letters, the digits 0 to 9 and _: first_name, order_id, _total. Letters of other languages are refused in names.

URLs

  • Link href, Button url and externalLink take http:, https:, mailto: and tel: URLs. Button url may also be empty, which draws the button with an empty link, as the editor does for a button with no URL. externalLink may be empty, for no link.
  • Image src and InlineImage src take https: URLs only. They may be empty, for the editor’s image placeholder.
  • webFontUrl takes an https: URL with none of ', ", (, ), <, >, \, ;, {, }, spaces or control characters.
  • A URL may hold {{ name }} pieces, one name in each, or be one {{ name }} alone. {{ }} holds no filters or other template syntax.
  • Relative URLs, other schemes such as javascript: and data:, a URL with a space at its start or end, and a URL that holds a control character, also inside a {{ }} piece, are refused. So is a {{ }} piece that holds a line or paragraph separator (U+2028, U+2029).
  • Write & in a URL as &amp;, as everywhere in XML.

Colors

A color is #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(r, g, b), rgba(r, g, b, a) or transparent. Section backgroundColor may also be empty, for no background. A read prints a color as it is stored.

Sizes

Sizes are plain numbers with no unit, and mean pixels. Column width is a percent of the row. Image width takes a percent such as 50%, a number of pixels or auto.

Font names

Unlike a name, a fontFamily takes letters of any language. It holds letters, digits, spaces, - and _ only, and starts with a letter or digit, as in Open Sans or Noto Sans JP. A fontFallback is one of the listed fonts.

Escaping, entities and comments

  • In text, write & as &amp; and < as &lt;. In attribute values, write & as &amp;, < as &lt; and " as &quot;.
  • The five XML entities (&lt;, &gt;, &amp;, &quot;, &apos;) and numeric references such as &#160; work. Other named entities, such as &nbsp;, and <!DOCTYPE> are refused.
  • An XML declaration, <?xml version="1.0"?>, may stand at the start, and is ignored. A declaration of another version, such as 1.1, is syntax. Other processing instructions are refused.
  • AhaML that holds a lone UTF-16 surrogate, half of a pair with no other half, is syntax at that place.
  • Comments are allowed, and dropped.

What a read prints

A read prints the stored design as the email draws it. So some designs come back in another form, which draws the same email, apart from the margins and the boxes noted below:
  • Text comes back by the rules of Text and whitespace.
  • A Color mark with no color comes back as the paragraph color, #374151. A Highlight with no color, an empty color and an image with no source draw nothing, and are left out.
  • A theme setting the stored design lacks comes back at the value the email is drawn with.
  • A section padding or margin, or a border width or radius, that the editor keeps as one value for all four sides comes back with all four sides at that value.
  • A setting an older editor stored that the email does not use, such as a color on a list item, is left out.
  • A paragraph, heading or footer whose text draws nothing, such as spaces alone, is left out. An empty paragraph, which draws a blank line, stays. When the only text is characters XML cannot hold and one of them draws a line, the block comes back as one line: an empty paragraph, or a heading or footer holding a <Br/>.
  • A rule whose result is known before the email is sent, such as one on a name that is not valid, is left out when it always shows its block, and the block is left out with it when it always hides it. A Repeat whose list name is not a valid name is left out with its blocks.
  • A rule whose action is neither show nor hide is left out, as the email always shows its block. A value stored with is_true or is_false is left out, as those operators do not read it.
  • Characters XML cannot hold, such as U+000B, are left out of text and attribute values. Most of them draw a small box in the email, which the read-back email lacks. U+0000 draws nothing, and U+000B and U+000C draw one only after text in a mark.
  • A {{ }} piece of a URL that the editor stored with the variable’s label comes back with the name alone, as {{ name }}.
A block left out as above can take its bottom margin with it where no block next to it draws as large a margin: at the start or the end of the tag that holds it, or after a Spacer, a Columns or a Section with no bottom margin. A design made by an older editor comes back with every setting it lacks at its default, as the editor fills them when it opens the design. This is the one case where an edit can change the sent email beyond what the edit itself changes: a write stores the filled settings, as the next save in the dashboard would. AhaML sent back unchanged keeps the stored design. A design AhaML cannot show reads with markup null and markup_error set:
  • no_design: the design has no blocks yet. A write with markup makes one.
  • html_block: the design holds an HTML block. Remove it in the dashboard to work on the design as AhaML.
  • unmapped_block: the design holds a block that AhaML has no tag for.
  • bad_stored_value: the design holds a value that AhaML cannot write. The message names the tag and the attribute. A section whose padding or border width the editor keeps as one value, but whose left and right sides still differ from it, gives this too: those sides size the section’s content.
  • renderer_unavailable: AhaML could not be made just now. Try the read again.
  • rate_limited: the account is over the limit of AhaML reads and writes. Try the read again later.

Limits

Each Link, Color and Highlight is copied onto every text and line break inside it, so a long href around many lines makes a large design.

Errors

A refused write answers 400 with the problems in document order, up to the first 100, and a message that says how many there are:
  • code and message are always there. line, column and path are there when the problem has a place in the AhaML: too_large, rendered_too_large and broken_template_syntax have none.
  • line and column start at 1 and count characters. They give the start of the problem: the < of a tag, the first letter of an attribute or the first character of a text.
  • path names each tag with its place among the siblings of the same tag.
  • hint is there when a fix is known.
  • AhaML that is not well-formed XML gives one problem, where reading stopped. A problem with a limit, a <!DOCTYPE>, an entity or a processing instruction also comes back alone.
A write that is taken can still give warnings, in markup_warnings, in the same form with no hint: