# Flyanote > Email for small businesses and community groups: newsletters, and receipts from your app or AI agent. Pay as you go. In development; private beta early 2027. Every page on flyanote.com in Markdown, in one file. `llms.txt` lists them: https://flyanote.com/llms.txt. The API describes itself in OpenAPI 3.1: https://flyanote.com/openapi.json. --- Source: https://flyanote.com/ In development · private beta early 2027 Email marketing and transactional email # You've got people to tell. New batch. Next meeting. Order received. Send newsletters and welcome emails. Send receipts and password resets from your app or agent. Built for small businesses and community groups. Pay for what you send. ## For developers and AI agents The API is at `https://app.flyanote.com/api`, with its [OpenAPI document](https://flyanote.com/openapi.json) and a [quick start](https://flyanote.com/docs/quickstart). The sandbox is free and never sends real mail. Today a person opens the account, by invite, and makes the key. Live mail needs a verified domain and credit, and opens with the private beta. Pay as you go, priced per email. [What an agent can do today](https://flyanote.com/developers) [Ask for an invite](mailto:hello@flyanote.com?subject=Flyanote%20invite)[Read our standards](https://flyanote.com/#standards) Email hello@flyanote.com and tell us what you'd send. We'll send invites as the private beta opens. ## Keep your people posted. Collect signups, write your email, and share what's happening: a new batch, your next club meeting, or this week's community news. Then see who it reached. ## Your code has people to tell, too. A clean API and an MCP server for developers and AI agents. Send receipts, password resets, and notifications, with a free sandbox that never sends real mail. ## Your budget has other jobs. Prepaid, pay-as-you-go credits priced at roughly our cost plus 10%. No monthly minimum, and credits don't expire. As we grow, the price goes down. ## Our standards A healthy inbox is everyone's business. Every Flyanote sender agrees to these rules, and we enforce them automatically. * **Permission only.** Recipients must have asked to hear from you. Double opt-in is on by default. Purchased, rented, or scraped lists are not allowed. * **Easy to leave.** Every marketing email has a one-click unsubscribe, and we honor it immediately across your account. * **Authenticated mail.** Senders authenticate their own domain with SPF, DKIM, and DMARC. Groups without a domain use a Flyanote address with limits sized for small organizations. * **Automatic protection.** Bounces and spam complaints are suppressed automatically. New senders start small, content is checked before it goes out, and accounts with high complaint or bounce rates are paused. Full details are in our [acceptable use policy](https://flyanote.com/acceptable-use). ## Contact General questions [hello@flyanote.com](mailto:hello@flyanote.com) Report spam or abuse [abuse@flyanote.com](mailto:abuse@flyanote.com) Mail delivery issues [postmaster@flyanote.com](mailto:postmaster@flyanote.com) Security reports [security@flyanote.com](mailto:security@flyanote.com) Privacy requests [privacy@flyanote.com](mailto:privacy@flyanote.com) If you received an email sent through Flyanote that you didn't ask for, forward it to abuse@flyanote.com. We read every report. --- Source: https://flyanote.com/acceptable-use Draft · last updated September 24, 2026 # Acceptable use policy Flyanote exists to help small organizations talk to people who want to hear from them. This policy covers everyone who sends through Flyanote: people using the app, developers using the API, and AI agents acting for a customer. The account owner is responsible for everything sent from their account. ## 1\. Permission * Send marketing email only to people who clearly agreed to receive it from you. Keep a record of when and how they agreed. * Do not use purchased, rented, shared, harvested, or scraped lists, or lists of people who never interacted with you. * Transactional email (receipts, password resets, event confirmations) must relate to something the recipient did. ## 2\. Every message * Identify who it's from, with an accurate From name, address, and subject line. * Marketing email must include a working one-click unsubscribe and a valid postal address where the law requires one. Honor unsubscribes immediately. * Follow the anti-spam and privacy laws that apply to you and your recipients, including CAN-SPAM, CASL, GDPR, and PECR. ## 3\. Content you can't send * Illegal content, or content promoting illegal activity. * Phishing, malware, deceptive links, or impersonation of another person, brand, or organization. * Sexually explicit material, and anything that exploits or endangers children. * Harassment, threats, or content that promotes violence or hatred against people based on protected characteristics. * High-risk offers such as get-rich-quick schemes, multi-level marketing recruitment, payday loans, debt relief, unlicensed pharmaceuticals, or gambling where not licensed. * AI-generated content presented to mislead recipients about who is speaking or what is true. ## 4\. Sending health * Authenticate your own sending domain (SPF, DKIM, and DMARC), or use a Flyanote address within its limits. * Keep bounce and spam-complaint rates low. We watch these automatically, and accounts that exceed safe thresholds are paused while we work with you. * New accounts start with modest sending limits that grow as they build a good record. ## 5\. Enforcement We check content and sending patterns automatically. When something looks wrong, we may pause a send, lower limits, or suspend an account, and we tell you why and how to fix it. Serious or repeated violations lead to account closure. Illegal content is reported to the authorities where required. ## 6\. Reporting If you received unwanted email sent through Flyanote, forward it to [abuse@flyanote.com](mailto:abuse@flyanote.com). Security issues go to [security@flyanote.com](mailto:security@flyanote.com). --- Source: https://flyanote.com/developers # Email for your app and your AI agent Flyanote's API sends transactional email, like receipts, password resets, and notifications, one recipient per message. Developers and AI agents use the same API, with a free sandbox that never sends real mail. Start with a free test send: the [quick start](https://flyanote.com/docs/quickstart) has the commands. Live mail is pay as you go, priced per email, with no monthly fee. ## Can an AI agent sign up and send through Flyanote with no person? Not yet. Today a person opens the account and makes the API key. With a test key, an agent can then work with the API on its own: send test messages, which are free and never delivered, and read what happened to them. Live mail also needs a live key and credit, which a person adds today, and a verified domain. It opens to customers with the private beta. ## Each step, and who takes it today ### In the sandbox, free 1. **Open an account** Who: a person. In a browser, with an invite: accounts are by invite while Flyanote is in development. Email hello@flyanote.com and tell us what you’d send. 2. **Make a test key** Who: a person (an owner or admin). In the app, under API keys. A test key (`fan_test_…`) is free and never delivers. It’s shown once. 3. **Send a test message** Who: the agent, with the test key. `POST /v1/messages` from `test@sandbox.flyanote.com` to a team member’s verified address. Free, and never delivered. 4. **See what happened** Who: the agent. `GET /v1/messages/{id}`. A test message ends as `test`. ### Live mail 1. **Make a live key** Who: a person (an owner or admin). In the app, the same way. A live key (`fan_live_…`) sends real email and uses credit. 2. **Add a sending domain** Who: the agent, with the live key and access to the domain’s DNS. `POST /v1/domains` returns the DNS records to add, and `POST /v1/domains/{id}/check` checks them. Without DNS access, a person adds the records. 3. **Add credit** Who: a person. While Flyanote is in development, we add credit on request. 4. **Send live mail** Who: the agent, with the live key. The same `POST /v1/messages`, from an address on the verified domain. Live mail opens to customers with the private beta. The [quick start](https://flyanote.com/docs/quickstart) has the commands for the test send and the domain. ## What changes before sign-up opens Sign-up opens early 2027\. Before then, an agent will be able to start an account through the API and get its own test key. A person still claims the account before it sends live mail. An MCP server is planned too. This page changes as each reaches production. ## What it costs Test sends are free. Live mail is pay as you go: prepaid credit, priced per email, with no monthly fee. We'll publish the price per 1,000 emails before sign-up opens. ## What the API does for an agent * Every send takes an `Idempotency-Key`. Send a retry with the same key and body within 24 hours, and it never sends twice. * Every error is an RFC 9457 problem with a stable `code`, what went wrong, and the steps that fix it, in order. * `GET /v1/api-keys/current` says what a key is and what it may do, before it tries anything. * A test key runs every check a live key does but one: a test send is never refused for an address on Flyanote's own abuse list. So a test send that passes doesn't prove the live one will. * Rate limits come in `RateLimit` headers, with `Retry-After` once you're past one. ## Where to start * [The quick start](https://flyanote.com/docs/quickstart): send a free test message that's never delivered, then add and verify your sending domain. * [The OpenAPI document](https://flyanote.com/openapi.json): every operation, in OpenAPI 3.1\. It's a copy, refreshed each time this site updates; [the API's own](https://app.flyanote.com/api/openapi) is always current. * The API's base URL is `https://app.flyanote.com/api`. Send the key in this header: `Authorization: Bearer ` * [API errors](https://flyanote.com/docs/errors): every code, and the steps that fix it. * For AI agents: [llms.txt](https://flyanote.com/llms.txt) lists every page. * For AI agents: [llms-full.txt](https://flyanote.com/llms-full.txt) has every page in one file. * Add `.md` to the address of any page on this site for its Markdown. * [All the docs](https://flyanote.com/docs). --- Source: https://flyanote.com/docs # Docs How to send email through Flyanote's API, for developers and AI agents. Start with the quick start: a test send is free, and never delivered. * [Quick start](https://flyanote.com/docs/quickstart): send a free test message that's never delivered, then add and verify your sending domain. * [API errors](https://flyanote.com/docs/errors): every code the API returns, and the steps that fix it. * [The email format](https://flyanote.com/docs/email-format): the JSON document an email is written in. * [Health checks](https://flyanote.com/docs/checks): what Flyanote checks in an email before it sends. * [The OpenAPI document](https://flyanote.com/openapi.json): every operation, copied from the running API each time this site updates. * [Developers and AI agents](https://flyanote.com/developers): what works today, and which steps need a person. For agents: `llms.txt` lists every page, `llms-full.txt` has them all in one file, and any page's address with `.md` added is its Markdown. --- Source: https://flyanote.com/docs/checks # Health checks Before an email goes out, Flyanote checks it: that it builds, that its merge tags work, that it fits in the inbox, that its links are sound, and that it has what the law requires. Each finding names its check. How serious a finding is depends on the check and on the email’s category, marketing or transactional. These are the severities: **Reject** (`reject`) The save is refused and nothing is stored. The API answers 422. **Block** (`block`) The draft saves, but it can’t be published, scheduled, or sent until it’s fixed. A block marked “also stops a test send” stops that too. **Warning** (`warning`) Worth a look first. The app asks you to confirm; the API sends and lists it in `warnings[]`. **Tip** (`info`) A suggestion. It never stops anything. **Our bug** (`bug`) A block that is our fault, not yours. We’re alerted, your draft is saved, and trying again usually works. **Added for you** (`applied`) A note that we changed something on our own, such as adding the footer the law requires. This page is generated from the checks themselves, so it lists exactly the checks we run. Each finding’s `docs_url` links to its check’s entry here, at `#`, such as `#compliance-identity` for `compliance.identity`. ## Checks by kind * [Building the email](https://flyanote.com/docs/checks#render): [render.schema](https://flyanote.com/docs/checks#render-schema), [render.context](https://flyanote.com/docs/checks#render-context), [render.compile](https://flyanote.com/docs/checks#render-compile), [render.invariants](https://flyanote.com/docs/checks#render-invariants) * [Merge tags and Liquid](https://flyanote.com/docs/checks#liquid): [liquid.syntax](https://flyanote.com/docs/checks#liquid-syntax), [liquid.profile](https://flyanote.com/docs/checks#liquid-profile), [liquid.unknown\_field](https://flyanote.com/docs/checks#liquid-unknown-field), [liquid.no\_fallback](https://flyanote.com/docs/checks#liquid-no-fallback), [liquid.render](https://flyanote.com/docs/checks#liquid-render), [liquid.missing\_data](https://flyanote.com/docs/checks#liquid-missing-data) * [Size](https://flyanote.com/docs/checks#size): [size.clip](https://flyanote.com/docs/checks#size-clip), [size.style](https://flyanote.com/docs/checks#size-style), [size.table](https://flyanote.com/docs/checks#size-table) * [Links](https://flyanote.com/docs/checks#link): [link.scheme](https://flyanote.com/docs/checks#link-scheme), [link.from\_contact](https://flyanote.com/docs/checks#link-from-contact), [link.empty](https://flyanote.com/docs/checks#link-empty), [link.http](https://flyanote.com/docs/checks#link-http), [link.shortener](https://flyanote.com/docs/checks#link-shortener), [link.text\_mismatch](https://flyanote.com/docs/checks#link-text-mismatch), [link.personal\_data](https://flyanote.com/docs/checks#link-personal-data), [link.utm](https://flyanote.com/docs/checks#link-utm), [link.broken](https://flyanote.com/docs/checks#link-broken), [link.unchecked](https://flyanote.com/docs/checks#link-unchecked), [link.dropped](https://flyanote.com/docs/checks#link-dropped) * [Words](https://flyanote.com/docs/checks#content): [content.subject\_length](https://flyanote.com/docs/checks#content-subject-length), [content.preheader](https://flyanote.com/docs/checks#content-preheader), [content.caps](https://flyanote.com/docs/checks#content-caps), [content.banned\_words](https://flyanote.com/docs/checks#content-banned-words), [content.preferred\_words](https://flyanote.com/docs/checks#content-preferred-words), [content.starter\_text](https://flyanote.com/docs/checks#content-starter-text), [content.review](https://flyanote.com/docs/checks#content-review), [content.empty](https://flyanote.com/docs/checks#content-empty) * [The law and the inbox](https://flyanote.com/docs/checks#compliance): [compliance.identity](https://flyanote.com/docs/checks#compliance-identity), [compliance.footer](https://flyanote.com/docs/checks#compliance-footer), [compliance.unsubscribe](https://flyanote.com/docs/checks#compliance-unsubscribe), [compliance.address](https://flyanote.com/docs/checks#compliance-address), [compliance.from](https://flyanote.com/docs/checks#compliance-from), [compliance.subject\_reply](https://flyanote.com/docs/checks#compliance-subject-reply), [compliance.tx\_promotional](https://flyanote.com/docs/checks#compliance-tx-promotional), [compliance.ai\_marking](https://flyanote.com/docs/checks#compliance-ai-marking) * [Accessibility](https://flyanote.com/docs/checks#a11y): [a11y.main\_heading](https://flyanote.com/docs/checks#a11y-main-heading), [a11y.heading\_order](https://flyanote.com/docs/checks#a11y-heading-order), [a11y.contrast](https://flyanote.com/docs/checks#a11y-contrast), [a11y.small\_text](https://flyanote.com/docs/checks#a11y-small-text), [a11y.text\_sizes](https://flyanote.com/docs/checks#a11y-text-sizes), [a11y.link\_text](https://flyanote.com/docs/checks#a11y-link-text), [a11y.table\_header](https://flyanote.com/docs/checks#a11y-table-header), [a11y.lang](https://flyanote.com/docs/checks#a11y-lang), [a11y.glyphs](https://flyanote.com/docs/checks#a11y-glyphs) * [Images](https://flyanote.com/docs/checks#img): [img.alt\_quality](https://flyanote.com/docs/checks#img-alt-quality), [img.too\_large](https://flyanote.com/docs/checks#img-too-large), [img.total](https://flyanote.com/docs/checks#img-total), [img.soft](https://flyanote.com/docs/checks#img-soft), [img.no\_width](https://flyanote.com/docs/checks#img-no-width), [img.format](https://flyanote.com/docs/checks#img-format), [img.animated](https://flyanote.com/docs/checks#img-animated), [img.heavy](https://flyanote.com/docs/checks#img-heavy), [img.only](https://flyanote.com/docs/checks#img-only), [img.external](https://flyanote.com/docs/checks#img-external) * [Your own HTML](https://flyanote.com/docs/checks#html): [html.balance](https://flyanote.com/docs/checks#html-balance), [html.unsupported](https://flyanote.com/docs/checks#html-unsupported), [html.width](https://flyanote.com/docs/checks#html-width), [html.hidden\_text](https://flyanote.com/docs/checks#html-hidden-text), [html.layout\_table\_role](https://flyanote.com/docs/checks#html-layout-table-role), [html.font](https://flyanote.com/docs/checks#html-font), [html.color](https://flyanote.com/docs/checks#html-color), [html.color\_added](https://flyanote.com/docs/checks#html-color-added) * [Dark mode](https://flyanote.com/docs/checks#dark): [dark.text\_contrast](https://flyanote.com/docs/checks#dark-text-contrast), [dark.image\_vanishes](https://flyanote.com/docs/checks#dark-image-vanishes), [dark.text\_over\_image](https://flyanote.com/docs/checks#dark-text-over-image), [dark.divider](https://flyanote.com/docs/checks#dark-divider), [dark.brand\_shift](https://flyanote.com/docs/checks#dark-brand-shift), [dark.unmodeled](https://flyanote.com/docs/checks#dark-unmodeled) ## Building the email Every email must build. These checks make sure the document is valid, everything it names exists, and what we build from it is sound. ### `render.schema`: The document isn’t valid The email document doesn’t match the format: a property is missing, misspelled, or the wrong type, or a rule across properties is broken. Each problem has a JSON Pointer and a code from the format’s error catalog. * For: marketing, transactional email * Runs at: save, validate * Version: 5 | Severity | When | | -------- | --------------------------------- | | Reject | Any problem: the save is refused. | #### How to fix it 1. Fix each problem in `errors`: `pointer` says where, and its `docs_url` links to the fix on the email format page. 2. Check a document without saving it with the validate call. ### `render.context`: Something the email names is missing The email names something your account doesn’t have: a brand kit color, font, or logo, an uploaded image, a saved link, or a merge tag Liquid can’t read. It can’t be built until that’s fixed. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 3 | Severity | When | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Block, also stops a test send | While drafting, and on a new publish. | | Warning | A published template whose kit or uploads changed since it was published: it still sends, using a fallback, and warns. | #### How to fix it 1. Pick something that exists, such as another kit color, or add what’s missing to your brand kit or uploads. 2. For a merge tag, fix the problem the finding names; its `code` is on the email format page. ### `render.compile`: We couldn’t build the email With the code `template_too_large`, the email is too big to build: its HTML, the code for its layout included, would be over the 500 KB a template can hold. With `colors_too_many`, it uses too many different colors: over 2,000 pairings of a color (text, border, or background) with the background behind it, each of which needs its own dark-mode color. Any other failure is on our side: it isn’t caused by anything you did, and we’ve been alerted. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 3 | Severity | When | | ----------------------------- | ------------------------------------------------------------------ | | Our bug | When the failure is ours. | | Block, also stops a test send | When the email is too big to build (template\_too\_large). | | Block, also stops a test send | When the email uses too many different colors (colors\_too\_many). | #### How to fix it 1. For `template_too_large`, shorten or remove sections, or replace long lists with links, then try again. 2. For `colors_too_many`, reuse a smaller set of text, border, and background colors. If you pasted or generated the HTML, simplify the colors in that source, then try again. A color used the same way on the same background counts once, however often it appears. 3. For any other failure, try again. Your draft is saved. 4. If that keeps happening, contact support with the `request_id`. Gmail also cuts off emails over about 100 KB. That limit is on the finished email, and we check it once the email builds. ### `render.invariants`: What we built broke a rule Every email we build is checked against rules we promise: a language, alt text on images, the footer in place, no scripts, and a plain-text part with every link. One failed, which is our bug. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 5 | Severity | When | | -------- | ------- | | Our bug | Always. | #### How to fix it 1. Try again. Your draft is saved. 2. If it keeps happening, contact support with the `request_id`. ## Merge tags and Liquid Merge tags fill in each person’s details. These checks catch tags that would fail or print nothing. ### `liquid.syntax`: A merge tag can’t be read A Liquid tag isn’t written correctly: a missing `}}` or `%}`, say, or a tag that’s never closed. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 3 | Severity | When | | ----------------------------- | ------------------------------------- | | Block, also stops a test send | While drafting, and on a new publish. | #### How to fix it 1. Fix the tag at the finding’s line and column, or remove it. ### `liquid.profile`: A Liquid feature we don’t support The email uses Liquid that our templates don’t allow, such as a tag or filter outside the supported set, or a brand value your kit doesn’t have. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 5 | Severity | When | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Block, also stops a test send | Always. | | Warning | A published template whose kit changed since it was published, so a brand value it names is gone: it still sends, with that value empty, and warns. | #### How to fix it 1. Use a supported tag or filter, or remove it. The email format page’s Liquid section explains each rule, such as `/docs/email-format#liquid-tag-unknown` for a tag we don’t support. ### `liquid.unknown_field`: A contact field that doesn’t exist A merge tag reads a `contact.*` field your contacts don’t have, so it would print nothing. If one is close, we suggest it. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Use the suggested field, or one your contacts have. 2. Or add the field to your contacts. ### `liquid.no_fallback`: A merge tag with no fallback If someone has no value for this field, nothing shows where it goes, which can leave a sentence broken. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | -------------------------------------------------------------- | | Warning | Marketing email. | | Tip | Transactional email, where your app usually sends every field. | #### How to fix it 1. Add a fallback: what to show when the field is empty, such as “there” in “Hi there”. ### `liquid.render`: The email failed to build for someone Filling in the email failed for some of the people or sample data we tried, so people with data like theirs would get nothing. * For: marketing, transactional email * Runs at: test send, publish and send, validate * Version: 2 | Severity | When | | ----------------------------- | ------- | | Block, also stops a test send | Always. | #### How to fix it 1. Check the field the finding names, and give it a fallback or simplify the Liquid that reads it. 2. Send a test with the same data to see it. ### `liquid.missing_data`: A field missing from a message’s data A transactional message’s `data` didn’t include a field the template reads, so that spot was left empty. * For: transactional email * Runs at: publish and send * Version: 2 | Severity | When | | -------- | -------------------------------------------------------- | | Warning | Each message sent without it. The message is still sent. | #### How to fix it 1. Send every field the template reads: its `meta.variables` lists them. 2. Or give the merge tag a fallback. ## Size Gmail cuts off emails over about 100 KB, hiding the end and the unsubscribe link. Sizes count the email as it will be sent, with names and tracked links filled in. ### `size.clip`: The email is too large for Gmail Gmail cuts off emails at about 100 KB and shows “Message clipped”, hiding the end of the email and its unsubscribe link. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 18 | Severity | When | | -------- | ------------------------------------------------------------------------------------------------------------------- | | Warning | From 80,000 bytes. | | Block | From 100,000 bytes. Transactional email blocks only when a template is published; a message is never held for size. | #### How to fix it 1. Remove or shorten sections. The finding lists the biggest ones. 2. Link to long content, such as a full list, instead of including it. ### `size.style`: Too many style rules Gmail ignores an email’s style rules past 16 KB, which turns off phone layouts and dark-mode colors. Each custom color adds rules. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 1 | Severity | When | | -------- | --------------------------------- | | Warning | From 14,000 bytes of style rules. | #### How to fix it 1. Use your brand kit’s colors instead of exact ones where you can. ### `size.table`: A large table One table adds a lot to the email’s size, bringing Gmail’s limit closer. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 18 | Severity | When | | -------- | -------------------------------- | | Tip | From 15,000 bytes for one table. | #### How to fix it 1. Link to the full list, or split it across emails. ## Links Links that are unsafe, empty, or misleading hurt how inboxes treat your mail, and they let readers down. ### `link.scheme`: A link type that isn’t allowed A link uses a scheme that’s never allowed, such as `javascript:` or `data:`, or it’s a relative address. In your own HTML, only `https:`, `http:`, `mailto:`, and `tel:` links are allowed. These are refused because they’re unsafe or don’t work in email. * For: marketing, transactional email * Runs at: save, validate * Version: 2 | Severity | When | | -------- | ------- | | Reject | Always. | #### How to fix it 1. Use a full web address starting `https://`, or a `mailto:` or `tel:` link. ### `link.from_contact`: A link address from contact data The start of a link’s address comes from a contact field. Anyone filling in your signup form can set that field, so they could point your email’s link anywhere. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Start the link with your own web address, and put contact data only after it. ### `link.empty`: A link that goes nowhere A link or button has no address yet, or still has the starter’s. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Add the address it should go to, or remove the link. ### `link.http`: A link that isn’t secure A link starts with `http:` rather than `https:`. Some inboxes and browsers warn readers before opening it. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Use the `https://` address, if the site has one. ### `link.shortener`: A link shortener A link goes through a shortener like bit.ly. Spam filters distrust them, since they hide where a link goes. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Use the full address the short link leads to. ### `link.text_mismatch`: Link text that names another site A link’s text reads as one website’s address, but the link goes to a different site. Readers and spam filters treat that as phishing. A link to another part of the same site is fine. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Make the text match where the link goes, or use words instead of an address. ### `link.personal_data`: A link that shares contact data A link to another website includes a contact field, such as an email address in its query string. In the EU and UK, sharing it that way needs the reader’s consent. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 2 | Severity | When | | -------- | --------------------------------------------------------------- | | Warning | Marketing email. | | Tip | Transactional email, where links like these are often expected. | #### How to fix it 1. Remove the field from the link, or make sure your readers have agreed to it. ### `link.utm`: Links without UTM tags Links to your website have no `utm_` tags, so your site’s analytics won’t count visits from this email. * For: marketing email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | -------- | ------- | | Tip | Always. | #### How to fix it 1. Turn on UTM tags in your account’s settings, and we add them to every link to your site. ### `link.broken`: A link that didn’t work When we checked a link, its site couldn’t be found, refused or dropped the connection, had a certificate error, answered that the page wasn’t found or is gone, or had a server error twice. Sites go down for a while, so this is a warning, never a block. * For: marketing, transactional email * Runs at: test send, publish and send, validate * Version: 3 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Open the link to see whether it works now. 2. If it doesn’t, fix the address or remove the link. If you run a website and found `FlyanoteLinkCheck` in your logs: it checks the links in an email one of our customers is about to send. It asks at most once a day for each link and account, whatever it found, a missing name or a refused connection included. After a timeout, a 429, or no route to your site, it asks again at the next check. For a link it sends `HEAD`, and only if that answers 400 or more, `GET` with `Range: bytes=0-0`. For an image it sends `GET` for the first 1 MB. It follows at most 5 redirects, and asks once more after a server error. No request carries cookies or credentials, and all come from AWS’s shared Lambda addresses in us-east-1. A 401, 403, or 429 answer is fine: we count the link as unchecked, never broken. ### `link.unchecked`: A link we couldn’t check We couldn’t check a link: its address is filled in from merge tags, its site didn’t let us in or didn’t answer in time, or it points somewhere we don’t fetch. * For: marketing, transactional email * Runs at: test send, publish and send, validate * Version: 3 | Severity | When | | -------- | ------- | | Tip | Always. | #### How to fix it 1. Send yourself a test and open the link. ### `link.dropped`: A link or image we left out A link or image gets its address from your data, and the data gave it a kind of address we don’t send. A link keeps only an https, http, mailto, or tel address, and an image only an https or http one, so the email goes without it: a link’s text stays and stops being a link, and an image isn’t shown. The finding gives each one’s place, never its address. * For: marketing, transactional email * Runs at: test send, publish and send, validate * Version: 1 | Severity | When | | -------- | --------------------------------------------------------------------------------------------------------------------------- | | Warning | A link or image left out. | | Our bug | We couldn’t check the email’s links with this data, so it can’t be shown or sent: a problem on our side, and we’re alerted. | #### How to fix it 1. Check the data you render or send with: the field behind that link or image should hold a full web address. 2. Render it again with the same data to see what a send would carry. ## Words Your subject, preview text, and copy, checked against common problems and your brand kit’s voice. ### `content.subject_length`: A long subject The subject is over 60 characters. Phones cut long subjects off. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------- | | Tip | Over 60 characters. | #### How to fix it 1. Put the key words first, or shorten it. ### `content.preheader`: Preview text missing, long, or repeated Inboxes show preview text beside the subject. Without it they show the email’s first words; past about 100 characters it’s cut off; and repeating the subject wastes the space. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Tip | Always. | #### How to fix it 1. Add preview text that says something the subject doesn’t, and put the key words first. ### `content.caps`: Capitals Capitals read as shouting, and spam filters notice them in subjects. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ---------------------------------------------------------------- | | Warning | A subject with 3 words in capitals in a row, or mostly capitals. | | Tip | A paragraph that is mostly capitals. | #### How to fix it 1. Write it in sentence case. ### `content.banned_words`: A word your brand kit avoids The email uses a word on your brand kit’s list of words to avoid. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Use another word, or change the list in your brand kit. ### `content.preferred_words`: Your brand kit spells it differently Your brand kit prefers another spelling or word for this one. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Tip | Always. | #### How to fix it 1. Use the kit’s spelling. ### `content.starter_text`: Starter text left in Part of the email is still the starter text it came with. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Replace it with your own words, or remove it. ### `content.review`: Text to check Part of the email was filled in for you, by AI or from a template, and nobody has checked it yet. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Warning | Always. | #### How to fix it 1. Read it, and change anything that isn’t right. ### `content.empty`: An empty email The email has nothing in it outside the footer. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ------- | | Block | Always. | #### How to fix it 1. Add a section with your message. ## The law and the inbox Marketing email must say who sent it and offer a way to unsubscribe. We add the footer that does both, and these checks make sure it’s there and readable. ### `compliance.identity`: Your organization’s details are missing The law requires marketing email to show who sent it: your organization’s name and postal address. We put them in your footer for you once you’ve added them. A test send shows a placeholder. * For: marketing email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ---------------- | | Block | Marketing email. | #### How to fix it 1. Add your organization’s name, postal address, and contact details in your account’s settings. ### `compliance.footer`: The footer Marketing email needs the footer we manage, with your details and the unsubscribe link. We add it if the email doesn’t have one. Its text must be at least 12px with a contrast of at least 4.5:1, in light mode and with our dark-mode colors, and if yours isn’t, we change it so it is. If it’s missing, hidden, or unreadable after that, it’s our bug. Some mail apps change its colors again in their own dark mode, and we warn where that likely takes it under 4.5:1. * For: marketing email * Runs at: save, preview, test send, publish and send, validate * Version: 4 | Severity | When | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Our bug | Missing, hidden, smaller than 12px, or under 4.5:1: our bug. | | Added for you | We added it. | | Warning | A mail app’s own dark mode takes it under 4.5:1\. We keep it readable in the apps that use our dark-mode colors; this warns about the rest. | #### How to fix it 1. Nothing, if it was added for you. 2. For our bug: try again, and contact support if it keeps happening. 3. For a dark-mode warning: edit the footer colors, then preview them in dark mode in the named apps. ### `compliance.unsubscribe`: The unsubscribe link Marketing email needs an unsubscribe link and a preferences link, both in the footer and in the plain-text part. We add them; if either is missing after that, it’s our bug. When the footer’s colors or size were too faint or small to read, we change them, and this says what we changed. * For: marketing email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | ------------- | --------------------------------------------------------------- | | Our bug | Either link missing from the footer or the plain text: our bug. | | Added for you | We changed the footer’s colors or size so it reads. | #### How to fix it 1. Nothing, if we changed it for you. To choose the change yourself, edit the footer’s colors or text size. 2. For our bug: try again, and contact support if it keeps happening. ### `compliance.address`: Your postal address Marketing email must show your organization’s name and postal address, and we put them in the footer. Transactional email shows them too unless you turn them off. When transactional email’s footer colors or size were too faint or small to read, we change them, and this says what we changed. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 3 | Severity | When | | ------------- | ------------------------------------------------------------------------ | | Our bug | Marketing email without them after we add them: our bug. | | Added for you | Transactional email: we added them. | | Added for you | Transactional email: we changed the footer’s colors or size so it reads. | #### How to fix it 1. Nothing, if they were added for you. In transactional email, you can leave them out in the footer’s settings. 2. Nothing, if we changed the footer for you. To choose the change yourself, edit the footer’s colors or text size. ### `compliance.from`: The sender’s name and address The sender name includes a web or email address that isn’t the one you send from, which inboxes treat as impersonation. Or marketing email comes from a no-reply address, so readers can’t answer. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ----------------------------------------- | | Warning | A sender name with another address in it. | | Tip | Marketing email from a no-reply address. | #### How to fix it 1. Use your organization’s name as the sender name, and send from an address someone reads. ### `compliance.subject_reply`: A subject that looks like a reply The subject starts with “RE:” or “FW:” (or the same in another language), making the email look like part of a conversation. Regulators treat that as misleading. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | -------------------- | | Warning | Marketing email. | | Tip | Transactional email. | #### How to fix it 1. Take it out of the subject. ### `compliance.tx_promotional`: Transactional email that reads like marketing A transactional email has offers in it, such as a discount code, a sale, or “Shop now”. Marketing needs an unsubscribe link and people’s agreement, so it can’t go out as transactional. * For: transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | -------------------- | | Warning | Transactional email. | #### How to fix it 1. Take the offers out, or send it as marketing email. ### `compliance.ai_marking`: Marked as written with AI Parts of this email were written with AI, so we mark it as AI-generated in its headers. * For: marketing, transactional email * Runs at: publish and send * Version: 1 | Severity | When | | ------------- | ----------------------------------------- | | Added for you | At send, when AI wrote part of the email. | #### How to fix it 1. Nothing: it’s a note. ## Accessibility Everyone should be able to read your email, including people using screen readers and people on small screens. None of these stops a send. ### `a11y.main_heading`: No single main heading Screen readers use the main heading (level 1) to say what an email is about. An email with headings should have exactly one. An email with no headings at all, such as a letter, gets only a tip. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ----------------------------------------------- | | Warning | Headings, but none at level 1 or more than one. | | Tip | No headings at all. | #### How to fix it 1. With no main heading: make the first heading level 1\. The finding’s fix does it. 2. With several: keep one at level 1 and make the rest level 2\. Each finding’s fix does it for its heading. ### `a11y.heading_order`: A heading skips a level A heading is more than one level below the one before it, such as a level 3 right after a level 1\. Screen reader users move through an email by its headings, and a gap suggests something is missing. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | --------------------------------------------------- | | Tip | Any skipped level, in text blocks or your own HTML. | #### How to fix it 1. Change the heading to the next level down. The finding’s fix does it. ### `a11y.contrast`: Text that’s hard to read Text, a link, or a button label doesn’t stand out enough from what’s behind it, by the WCAG 2.2 contrast ratio in light mode. Text over a background image is checked against the section’s background color, which shows while images are off. Dark mode has its own checks. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | -------------------------------------------------------------------------------------------------------- | | Warning | Under {a11y.min\_contrast}:1, or {a11y.min\_contrast\_large}:1 for large text (24 px, or 18.66 px bold). | #### How to fix it 1. Use the suggested color: the nearest one that passes. When the color is set on the element, the finding’s fix applies it. 2. Or change the background, or pick a brand kit color with more contrast. ### `a11y.small_text`: Small text Body text this small is hard to read on a phone. The footer is left out: it’s small by design. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------------------ | | Tip | Under {a11y.min\_body\_px} px. | #### How to fix it 1. Make the text larger. When the size is set on the element, the finding’s fix sets it to {a11y.min\_body\_px} px. ### `a11y.text_sizes`: Many text sizes The email uses a lot of different text sizes, which makes it look busy and harder to scan. Your brand kit’s sizes for headings and body text are usually enough. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------------------------------------------------- | | Tip | More than {a11y.max\_text\_sizes} sizes, the footer left out. | #### How to fix it 1. Use your brand kit’s sizes instead of setting one on each piece of text. The finding lists the sizes. ### `a11y.link_text`: Link text that doesn’t say where it goes A link or button says only “click here”, “read more”, or the same in the email’s language. Screen reader users often list an email’s links on their own, where words like that say nothing. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | -------------------------------------------- | | Tip | The whole link text is one of those phrases. | #### How to fix it 1. Say what’s at the other end, such as “See the class schedule”. ### `a11y.table_header`: A table column without a header Every table has a header row, and some of its cells are empty. On phones, each value is shown with its column’s header as a label, and so is the plain-text part, so a column without one has values with no label. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ---------------------- | | Warning | Any empty header cell. | #### How to fix it 1. Give every column a header. ### `a11y.lang`: The language doesn’t match the text The email’s language setting doesn’t match the language its text is in. Screen readers pronounce text by that setting, and some inboxes use it to offer a translation. We only say so when the text clearly reads as another language. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | -------------------------------------------------------------------- | | Tip | The text clearly reads as another language, or is in another script. | #### How to fix it 1. Change the email’s language. When we can tell which language it is, the finding’s fix sets it. ### `a11y.glyphs`: A font without the characters it needs Some text is in a writing system, such as Hebrew, Arabic, or Thai, that its font has no characters for, so inboxes show that text in a different font. Another font on our list has them. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ---------------------------------------------------------------------------------------------------- | | Warning | When another font on our list covers the writing system. If none does, inboxes use the reader’s own. | #### How to fix it 1. Change the font to the one the finding names, for this text or for the whole email. ## Images Many inboxes hide images until the reader allows them, and large files load slowly on phones. These checks look at what each image says, weighs, and needs. ### `img.alt_quality`: An image description that doesn’t describe it An image’s description (its alt text) is a file name, a web address, or a word like “image” or “logo” on its own. People using screen readers, and anyone with images off, get the description instead of the image. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | --------------------- | | Warning | Any such description. | #### How to fix it 1. Say what the image shows, or what it’s for, in a few words. 2. If it’s only decoration, mark it decorative instead. ### `img.too_large`: A large image file A large image loads slowly on phones and mobile data, and some readers give up before it does. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | --------------------- | | Warning | Over 1,000,000 bytes. | #### How to fix it 1. Use a smaller file: a JPEG for photos, saved at the size it shows at, or twice that for sharp screens. ### `img.total`: Heavy images in total The email’s images add up to a lot to download, which is slow on mobile data. Each file counts once. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ---------------------------- | | Tip | Over 3,000,000 bytes in all. | #### How to fix it 1. Use fewer images, or smaller files. ### `img.soft`: An image that may look soft The image file isn’t much wider than the space it fills, so it may look blurry on sharp (retina) screens, which need about twice the pixels. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------------------------- | | Tip | Under 1.5 times as wide as the space. | #### How to fix it 1. Use a file about twice as wide as the space. The finding suggests a width. ### `img.no_width`: An image in your HTML with no width An image in your own HTML has no width in pixels. Classic Outlook then shows it at the file’s full size, which can break the layout. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ----------------------------------------- | | Warning | An without a pixel width attribute. | #### How to fix it 1. Add a `width` attribute in pixels. The finding suggests one when we know the file’s size. ### `img.format`: An image format some inboxes can’t show WebP, AVIF, HEIC, SVG, BMP, and TIFF images don’t show as they are in every inbox we test against, by caniemail.com’s data: classic Outlook can’t show WebP, AVIF, HEIC, or TIFF, and Gmail turns SVG into a flat picture. The finding names the inboxes. Uploaded images are converted for you; this is for images from other websites or your own HTML. * For: marketing, transactional email * Runs at: save, test send, publish and send, validate * Version: 1 | Severity | When | | -------- | ---------------------------------------------------------------------------------------------------------------------- | | Warning | Any such image. While drafting it goes by the address’s file extension; at a test send and a send, by the file itself. | #### How to fix it 1. Use a JPEG, PNG, or GIF, or upload the image. ### `img.animated`: An animated image Classic Outlook shows only the first frame of an animated GIF. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------- | | Tip | Any animated image. | #### How to fix it 1. Make sure the first frame says what matters on its own. ### `img.heavy`: Mostly images Images cover most of the email. Readers with images off see much less, and spam filters treat image-heavy mail with suspicion. Images we don’t know the size of aren’t counted. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------------------------------------------------- | | Warning | Marketing email whose images cover more than 0.6 of its area. | | Tip | The same, in transactional email. | #### How to fix it 1. Move the key words out of images and into text. ### `img.only`: Almost all images The email has images but almost no text outside the footer. Many inboxes hide images at first, so readers see next to nothing, and spam filters distrust it. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ----------------------------------------------------------- | | Warning | Fewer than 100 characters of text, with at least one image. | #### How to fix it 1. Add text that says what the email is about. ### `img.external`: An image from another website The image loads from another website, or from an address a merge tag fills in. If that site moves or removes it, it breaks in every copy of the email, even ones already sent. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------------------------------------------------ | | Tip | Any image that isn’t uploaded, except your brand kit’s logo. | #### How to fix it 1. Upload a copy, so it’s served with your email. ## Your own HTML HTML you write yourself is checked when you save it, and anything unsafe is refused. These checks catch what saves but may not show as you meant, in the inboxes we test against. ### `html.balance`: An element left open Your HTML opens an element or a comment it never closes. We close it at the end of your HTML, so it can’t spill into the rest of the email, but the result may not be what you meant. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 1 | Severity | When | | -------- | --------------------------------- | | Warning | Any element or comment left open. | #### How to fix it 1. Add the closing tag where it belongs. The finding points at where the element opens. ### `html.unsupported`: HTML or CSS some inboxes don’t support Your HTML uses an element, attribute, or CSS property that an inbox we test against doesn’t support, by caniemail.com’s data. The finding names those inboxes. What they support only partly is one tip for the whole piece of HTML. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 1 | Severity | When | | -------- | ---------------------------------------------------- | | Warning | Not supported in at least one inbox we test against. | | Tip | Only partly supported. | #### How to fix it 1. Use something that works everywhere, or check the email in a test send to those inboxes. ### `html.width`: Too wide for a phone Part of your HTML has a fixed width a phone can’t fit, and phones can’t scroll sideways in an email. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 1 | Severity | When | | -------- | ------------------------------------------------------------------------ | | Warning | Wider than 600 px, or wider than 375 px with no percentage or max-width. | #### How to fix it 1. Use 600 px or less with a `max-width`, or a percentage width. ### `html.hidden_text`: Hidden text Your HTML has text readers can’t see: hidden, transparent, sized to nothing, or almost the color of what’s behind it. Spam filters treat hidden text as a trick. * For: marketing, transactional email * Runs at: save, preview, publish and send, validate * Version: 1 | Severity | When | | -------- | ------------------------------------------------------------------- | | Warning | Any hidden text, including text under 1.5:1 against its background. | #### How to fix it 1. Remove it. To set the text inboxes show beside the subject, use the preview text field. ### `html.layout_table_role`: A table without a role A table in your HTML has no header cells and no `role="presentation"`, so screen readers read it cell by cell, as if it held data. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 1 | Severity | When | | -------- | --------------- | | Tip | Any such table. | #### How to fix it 1. If it’s for layout, add `role="presentation"`. If it holds data, give it header cells (``). ### `html.font`: A font not on our list Your HTML names a font that isn’t one of the fonts installed with Windows and macOS, so most readers see the next font in its list instead. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 1 | Severity | When | | -------- | ---------------------------------------------------- | | Tip | A font list that starts with a font not on our list. | #### How to fix it 1. Start the font list with a font from our list, or check that the fallback looks right. ### `html.color`: Colors rewritten in your HTML Your HTML wrote some colors as names, RGB or HSL values, or three-digit hex. We wrote them as six-digit hex so dark mode can adjust them; they look the same. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | ------------- | ----------------------------------- | | Added for you | When we rewrote at least one color. | #### How to fix it 1. Nothing: it’s a note. ### `html.color_added`: Colors added to your HTML A link in your HTML had no color, or some text sat on a background of its own with no color. A link with no color shows each inbox’s default blue, which dark mode can’t adjust, so we gave it your email’s link color (your brand kit’s, unless the email sets its own). Text on its own background keeps the color it already had, written out so dark mode can adjust it for that background. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 1 | Severity | When | | ------------- | ------------------------------------------------------------ | | Added for you | When we gave at least one link or background’s text a color. | #### How to fix it 1. Nothing: it’s a note. To use another color for a link, give the link a color of its own. ## Dark mode Mail apps in dark mode change your email’s colors, each in its own way, and as far as we know, leave images as they are. We predict what Apple Mail, Gmail, and Outlook do from published models, and these checks warn where light mode works and dark mode likely doesn’t. Each finding names the apps and never stops a send, and in the API its \`confidence\` says how sure the prediction is. ### `dark.text_contrast`: Text that’s hard to read in dark mode Text, a link, or a button label reaches the contrast it needs in light mode ({a11y.min\_contrast}:1, or {a11y.min\_contrast\_large}:1 for large text), but not after a mail app’s dark mode changes its colors. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 2 | Severity | When | | -------- | -------------------------------------------------------------------------------------------------------------- | | Warning | Under the contrast it needs in at least one app’s dark mode. | | Tip | Only in the apps that use our dark-mode colors, which we’re still tuning: a note, since it isn’t yours to fix. | #### How to fix it 1. Use the suggested color: the nearest one that passes in light mode and in each app we predict. 2. Set the color for dark mode in your brand kit, for the apps that use it. ### `dark.image_vanishes`: An image that disappears in dark mode An image with a transparent background shows against a light background but not against the one a mail app puts behind it in dark mode, like a black logo on a dark gray page. We read it from the image’s own pixels. That analysis is coming, for uploads and, when sending, for images from other sites; until it’s in, this check finds nothing. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | -------- | ------------------------------------------------------------------------------- | | Warning | Most of the image no longer stands out from the background in at least one app. | | Tip | An image with a white border shows as a white box on a dark background. | #### How to fix it 1. Add a light outline around the image. 2. For your logo, add a version for dark backgrounds to your brand kit. 3. For a white box, use an image with a transparent background. ### `dark.text_over_image`: Text over an image in dark mode Mail apps change the color of text but never the image behind it, so text that was readable over a picture may not be. We check it against the background color shown with images off and, once we analyze images’ pixels, against the image’s lightest or darkest part. * For: marketing, transactional email * Runs at: save, preview, test send, publish and send, validate * Version: 2 | Severity | When | | -------- | --------------------------------------------------------------- | | Warning | Readable in light mode and not in at least one app’s dark mode. | #### How to fix it 1. Put the text on a solid panel, which changes color with it. 2. Move the text below the image. ### `dark.divider`: A line that disappears in dark mode A divider or border you can see in light mode almost disappears in dark mode. Classic Outlook darkens the background but leaves lines as they are, so a dark line vanishes there. A button’s outline needs 3:1. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 2 | Severity | When | | -------- | ------------------------------------------------------------------- | | Warning | At least 1.5:1 in light mode and under 1.3:1 in an app’s dark mode. | #### How to fix it 1. Use the suggested color. 2. Draw the divider as a thin band of background color instead. ### `dark.brand_shift`: A brand color that changes in dark mode Gmail and classic Outlook change colors themselves in dark mode, and a brand color can come out as another color, like yellow turning brown. Apple Mail and Outlook.com use the dark-mode colors in your brand kit instead. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 2 | Severity | When | | -------- | ------------------------------------------------------- | | Warning | It becomes another named color. | | Tip | Only its lightness flips, like navy turning light blue. | #### How to fix it 1. Preview it in dark mode in the named apps. If you’re happy with it, leave it. 2. For a decorative area whose color matters, an image keeps it in every app, since apps leave images as they are. Keep text off it. ### `dark.unmodeled`: HTML we can’t predict in dark mode Your HTML uses styles we can’t work out, such as CSS variables, calc(), or selectors our model doesn’t follow, so we can’t say what dark mode does to it. * For: marketing, transactional email * Runs at: save, preview, validate * Version: 2 | Severity | When | | -------- | -------------------------- | | Tip | Any part we can’t predict. | #### How to fix it 1. Send yourself a test and open it in the named apps with dark mode on. --- Source: https://flyanote.com/docs/email-format # The email format Every Flyanote email is one JSON document. The editor, the API, AI drafts, and the compiler all read and write the same format, so an email you build in one works in the others. The document says what the email holds; your brand kit says how it looks. We turn the two into HTML that works across email clients. This page describes version `1.1`. Versions before 1.0 are drafts and may still change. The tables, limits, and errors below are generated from the format itself. ## On this page * [The schema](https://flyanote.com/docs/email-format#schema-urls) * [The document](https://flyanote.com/docs/email-format#top-level) * [Sections](https://flyanote.com/docs/email-format#sections) * [Blocks](https://flyanote.com/docs/email-format#blocks) * [Text](https://flyanote.com/docs/email-format#text-model) * [Values and styles](https://flyanote.com/docs/email-format#values) * [Links](https://flyanote.com/docs/email-format#links) * [Merge tags and Liquid](https://flyanote.com/docs/email-format#liquid) * [Ids](https://flyanote.com/docs/email-format#ids) * [Limits](https://flyanote.com/docs/email-format#limits) * [Versions](https://flyanote.com/docs/email-format#versioning) * [Errors](https://flyanote.com/docs/email-format#errors) ## The schema The format is published as a JSON Schema (draft 2020-12). Point your validator or your editor at it, and set `schema_version` in every document. The schema checks a document's structure: its fields, types, lengths, and patterns. The rules across fields, such as unique ids or a table's cells, run when you save or validate, and each refusal is one of the error codes below, so a document the schema accepts can still be refused. A few checks need your account, such as whether a brand kit token or an uploaded image exists. * * * * * * * * * * Under `llm/` are the flatter profiles an AI model writes, a whole draft (`.draft`) or a change to one (`.patch`). They have versions of their own, and each says which version of the format it expands into. `index.json` lists every version and its files. ## The document A document has a `schema_version`, a `meta` object, optional `settings`, and its `sections`, top to bottom. Every object is closed: a property the format doesn’t have is an error, never ignored. Names are snake\_case. ### The document An email, as the editor, the API, and AI write it. This schema checks its structure: shapes, types, lengths, and patterns. The rules across properties (ids, widths, a table’s cells, the footer, loop scopes, and Liquid) are the API’s validation, whose error codes are listed at https://flyanote.com/docs/email-format, so a document this schema accepts may still be refused. | Property | Takes | | -------------------------- | ---------------------------------------------------------------------------- | | schema\_version (required) | "1.1" | | meta (required) | Meta | | settings | Settings | | sections (required) | a list of up to 100: ColumnsSection, GridSection, HeroSection or HtmlSection | ### meta | Property | Takes | | ------------------- | ----------------------------------------------- | | subject (required) | text, up to 200 characters | | preheader | text, at least 1 and up to 200 characters | | lang (required) | text, up to 35 characters | | dir | one of "ltr" or "rtl" | | category (required) | one of "transactional" or "marketing" | | variables | a list of up to 500: text, up to 300 characters | | provenance | Provenance | `category` sets the rules. Marketing mail gets the footer the law requires, with your organization’s name, its postal address, and an unsubscribe link, plus the unsubscribe headers inboxes look for. Transactional mail, like a receipt, has no unsubscribe link, and shows your name and address unless its footer block turns them off. `variables` is worked out on every save from the merge tags the email uses; anything you send there is replaced. ### settings | Property | Takes | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | width | a whole number from 480 to 700 | | brand | text. The brand kit this email uses, by its id (kit\_…). Absent, the email follows the account’s default brand, whichever that is when it’s built. | | palette | text. One of the brand kit’s palettes, by id. Absent, main. | | theme | text. One of the palette’s themes: light (its colors as they stand), dark (its dark values), or the id of one of its themes. Absent, light. | | kit\_version | a whole number from 1 to 9,007,199,254,740,991\. The version of the brand kit this email was made against: one pin covers the brand, its palette, and its theme. Needs brand, since a version means nothing without its kit. Absent, the email follows the kit’s newest version. | | background | [Color](https://flyanote.com/docs/email-format#value-color) | | content\_background | [Color](https://flyanote.com/docs/email-format#value-color) | | color | [Color](https://flyanote.com/docs/email-format#value-color) | | link\_color | [Color](https://flyanote.com/docs/email-format#value-color) | | link\_underline | true or false | | font\_heading | [Font](https://flyanote.com/docs/email-format#value-font) | | font\_body | [Font](https://flyanote.com/docs/email-format#value-font) | | mobile | an object with h1, h2, h3, body, button, footer, side\_padding and buttons\_full\_width | | dark | an object with background, content\_background, color and link\_color | ## Sections A section is a full-width band of the email. Its `kind` says what it is: columns side by side, a grid of items, a hero over a background image, or your own HTML. ### columns section | Property | Takes | | ------------------ | ------------------------------------------------ | | kind (required) | "columns" | | id (required) | an id | | label | text, at least 1 and up to 60 characters | | provenance | Provenance | | columns (required) | a list of up to 4: Column | | widths | a list of up to 4: a whole number from 15 to 100 | | mobile | an object with stack and reverse | | style | SectionStyle | ### grid section | Property | Takes | | ------------------- | --------------------------------------------------------------- | | kind (required) | "grid" | | id (required) | an id | | label | text, at least 1 and up to 60 characters | | provenance | Provenance | | per\_row (required) | one of 2, 3 or 4 | | mobile | an object with per\_row | | gap | [Spacing](https://flyanote.com/docs/email-format#value-spacing) | | items (required) | a list of up to 24: GridItem | | style | SectionStyle | ### hero section One column over a background image. | Property | Takes | | ----------------- | --------------------------------------------------------------------------------------------- | | kind (required) | "hero" | | id (required) | an id | | label | text, at least 1 and up to 60 characters | | provenance | Provenance | | blocks (required) | a list of up to 8: TextBlock, ImageBlock, ButtonBlock, DividerBlock, SpacerBlock or LogoBlock | | style (required) | HeroStyle | ### html section A whole section of the customer’s own HTML with Liquid. | Property | Takes | | --------------- | --------------------------------------------- | | kind (required) | "html" | | id (required) | an id | | label | text, at least 1 and up to 60 characters | | provenance | Provenance | | html (required) | text, at least 1 and up to 100,000 characters | | style | HtmlSectionStyle | ### A column | Property | Takes | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | id (required) | an id | | blocks (required) | a list of up to 50: TextBlock, ImageBlock, ButtonBlock, DividerBlock, SpacerBlock, SocialBlock, LogoBlock, FooterBlock, HtmlBlock, RepeatBlock or TableBlock | | style | ColumnStyle | ## Blocks Blocks hold the content, and a block’s `type` says what it is. Columns take every block. Hero sections take `text`, `image`, `button`, `divider`, `spacer` and `logo`. Grid items and repeats take `text`, `image`, `button`, `divider` and `spacer`. A repeat can’t hold another repeat. The footer block goes in the last section, at most once. ### text block | Property | Takes | | ---------------- | ------------------------------------------------------------------------------------ | | type (required) | "text" | | id (required) | an id | | provenance | Provenance | | lines (required) | a list of up to 200: ParagraphLine, HeadingLine, BulletLine, NumberLine or QuoteLine | | style | TextStyle | ### image block | Property | Takes | | --------------- | --------------------------------------------------------- | | type (required) | "image" | | id (required) | an id | | provenance | Provenance | | src (required) | [Src](https://flyanote.com/docs/email-format#value-src) | | alt | text, up to 300 characters | | decorative | true | | width | text | | href | [Href](https://flyanote.com/docs/email-format#value-href) | | style | ImageStyle | ### button block | Property | Takes | | ---------------- | --------------------------------------------------------- | | type (required) | "button" | | id (required) | an id | | provenance | Provenance | | label (required) | text, up to 80 characters | | href (required) | [Href](https://flyanote.com/docs/email-format#value-href) | | variant | one of "fill" or "outline" | | width | text | | style | ButtonStyle | ### divider block | Property | Takes | | --------------- | ------------ | | type (required) | "divider" | | id (required) | an id | | provenance | Provenance | | style | DividerStyle | ### spacer block | Property | Takes | | ----------------- | --------------------------------------------------------------- | | type (required) | "spacer" | | id (required) | an id | | provenance | Provenance | | height (required) | [Spacing](https://flyanote.com/docs/email-format#value-spacing) | | style | SpacerStyle | ### social block | Property | Takes | | ------------------- | --------------------------------------------------- | | type (required) | "social" | | id (required) | an id | | provenance | Provenance | | networks (required) | a list of up to 12: an object with network and href | | icon\_style | one of "color", "dark" or "light" | | icon\_size | text | | style | SocialStyle | ### logo block | Property | Takes | | --------------- | --------------------------------------------------------- | | type (required) | "logo" | | id (required) | an id | | provenance | Provenance | | width | text | | href | [Href](https://flyanote.com/docs/email-format#value-href) | | style | LogoStyle | ### footer block | Property | Takes | | --------------- | --------------------------------- | | type (required) | "footer" | | id (required) | an id | | provenance | Provenance | | note | a list of up to 10: ParagraphLine | | show\_social | true or false | | show\_address | true or false | | style | FooterStyle | ### html block The customer’s own HTML with Liquid, linted. Not editable visually. | Property | Takes | | --------------- | -------------------------------------------- | | type (required) | "html" | | id (required) | an id | | provenance | Provenance | | html (required) | text, at least 1 and up to 50,000 characters | ### repeat block | Property | Takes | | ----------------- | ----------------------------------------------------------------------------------- | | type (required) | "repeat" | | id (required) | an id | | provenance | Provenance | | over (required) | text | | as (required) | text | | limit | a whole number from 1 to 200 | | blocks (required) | a list of up to 10: TextBlock, ImageBlock, ButtonBlock, DividerBlock or SpacerBlock | ### table block | Property | Takes | | ------------------ | ------------------------------------------------- | | type (required) | "table" | | id (required) | an id | | provenance | Provenance | | caption | text, at least 1 and up to 300 characters | | columns (required) | a list of up to 4: an object with align and width | | header (required) | a list of up to 4: Cell | | rows (required) | a list of up to 200: TableRow | | mobile | an object with layout | | style | TableStyle | ## Text A text block is a list of `lines`, and each line is a list of `spans`. A line’s `kind` is one of `p`, `h1`, `h2`, `h3`, `bullet`, `number` and `quote`. Lists are flat: consecutive `bullet` and `number` lines make one list, a line at `level` 2 nests under the line before it, and `start` begins a new numbered list. Consecutive `quote` lines make one quote. A line with no spans is an empty line. A span is either text or a field. A text span’s `text` may hold `\n` for a line break, but never `{{` or `{%`. A field span fills in a value, such as `contact.first_name`, with a `fallback` for when it’s empty and an optional `format`: `date`, `datetime`, `number` and `money`. `money` needs a `currency` (an ISO 4217 code, or a path to one) and reads the value as a whole number of that currency’s minor units: `1999` with `USD` shows $19.99\. Both kinds take a `link`, a `color`, a `highlight`, a `size`, and `marks`: `bold`, `italic`, `underline`, `strike`, `sup` and `sub`. When you send `{{ contact.first_name }}` in text, we turn it into a field span for you. Any other Liquid in text is an error. ## Values and styles Most styles take a token from your brand kit, such as `brand.primary` or `space.md`, or an exact value, such as a `#rrggbb` color or `24px`. A token follows your kit when it changes; an exact value stays as written. Leave a style out to use the kit’s default. A style that isn’t set on an element comes from its column, then its section, then `settings`, then your kit. Every element with a color also takes `dark`, exact colors for dark mode. Without them, we work out a readable dark version of each color. `Color` A palette token (brand.primary, text.default, extra.) or an exact color, #rrggbb. No alpha. `Font` font.heading, font.body, or a font family from the catalog. The catalog is checked in context. `PhoneFontSize` A size on phones: whole pixels from 12px to 72px. `PhonePadding` Side padding on phones, whole pixels from 0px to 200px: the left and right padding of each block inside that sets neither side itself. `Hex` An exact color, #rrggbb. Dark overrides take only these. `Href` https:, http:, mailto:, tel:, asset:ast\_, kit:website, kit:links/, kit:social/, system:unsubscribe, system:preferences, system:view\_online, or a merge tag. `FontSize` A size token (size.body, size.h2) or whole pixels from 10px to 72px. `Path` A data path, such as contact.first\_name, data.order.total, or item.name. `Spacing` A space token (space.md) or whole pixels from 0px to 200px. `Src` asset:ast\_, an http(s):// address, or a merge tag. `Radius` A radius token (radius.md) or whole pixels from 0px to 200px. ## Links A link is one string, and its scheme says where it goes: a web page (`https:`), an email address (`mailto:`), a phone number (`tel:`), an uploaded file (`asset:` and the file’s id, `asset:ast_…`), a link saved in your brand kit (`kit:website`, `kit:links/donate`, `kit:social/instagram`), or one we fill in (`system:unsubscribe`, `system:preferences`, `system:view_online`). A kit link follows your kit when it changes. * `http:` works, with a warning: use `https:` where you can. * `javascript:`, `data:`, `file:`, `vbscript:`, and relative links are errors. * The unsubscribe and preferences links are for marketing mail only. * A link can be a merge tag, such as `https://example.com/orders/{{ data.order.id }}`. A link that’s wholly a merge tag is checked again once it’s filled in. ## Merge tags and Liquid Merge tags fill in each person’s details. They read from `contact`, `account`, `message`, `campaign`, `data` (what you send with a transactional email), `links`, `now`, and, in your own HTML, `brand`. Where they may go: | Where | What it takes | | --------------------------------------------------------------------------------- | ------------------------------------------------------------- | | meta.subject, meta.preheader, an image’s alt, a button’s label, a table’s caption | Output tags: {{ path \| filter: value }}, no {% %} tags | | href, src, background\_image.src | Output tags, for links and images that differ for each person | | Text spans | None: use a field span | | A field span’s field and currency, a repeat’s over | A path, such as data.order.items, not Liquid | | Your own HTML, in an html block or section | Liquid, with brand too | | Styles, ids, settings, and fallbacks | Never | A repeat runs its blocks once for each item in a list, naming each one `as`: a receipt’s line items, say. That name can’t be one of the namespaces above. Transactional mail may read only `links.view_in_browser_url` from `links`. ## Ids Every section, column, grid item, and block has an `id`: `sec_`, `col_`, `itm_`, or `blk_`, then 8 lowercase letters and digits (Crockford base32, so no `i`, `l`, `o`, or `u`). An id is unique within its document. The API fills in any you leave out and returns them, so you can change one part of an email later. A copied element gets a new id. Lines, spans, rows, and cells have no ids. ## Limits These limits only stop abuse. The limit that matters is the size of the finished email: see [the size checks](https://flyanote.com/docs/checks#size). | At most | Of | | --------- | ------------------------------------ | | 1,000,000 | bytes in the whole document, as JSON | | 100 | sections in an email | | 50 | blocks in a column | | 4 | columns in a columns section | | 24 | items in a grid | | 6 | blocks in a grid item | | 8 | blocks in a hero | | 200 | lines in a text | | 200 | spans in a line | | 5,000 | characters in a span | | 10 | lines in the footer’s note | | 50,000 | characters in a custom HTML block | | 100,000 | characters in a custom HTML section | | 4 | columns in a table | | 200 | rows in a table | | 50 | spans in a table cell | | 10 | blocks in a repeat | | 200 | times a repeat runs | | 12 | networks in a social block | | 2,048 | characters in a link | ## Versions `schema_version` is `MAJOR.MINOR`. Until 1.0, versions are drafts and may change in place. From 1.0 on, a published version’s schema never changes: * Something new, such as a block or a property, is a new minor version. Documents you already have stay valid. * Renaming, removing, or tightening anything is a new major version, and we move saved emails forward for you. * `/schemas/email/v1.json` is always the newest 1.x version. Pin an exact version, such as `v1.0.json`, if you need it never to change. Error codes never change meaning. A new kind of error gets a new code. ## Errors When a document doesn’t fit the format, each error has a `code`, a JSON Pointer to where it is in what you sent, the fix, and a `docs_url` that links to the code’s entry below. Branch on the code. Each entry is at `#`, such as `#color-invalid` for `color_invalid`. Errors from the API itself, such as a missing key, are on [API errors](https://flyanote.com/docs/errors). ### Errors by kind * [Structure](https://flyanote.com/docs/email-format#errors-structure) * [Types of element](https://flyanote.com/docs/email-format#errors-types-of-element) * [Values](https://flyanote.com/docs/email-format#errors-values) * [Links and sources](https://flyanote.com/docs/email-format#errors-links-and-sources) * [Text and merge tags](https://flyanote.com/docs/email-format#errors-text-and-liquid) * [Rules across properties](https://flyanote.com/docs/email-format#errors-rules-across-properties) * [Custom HTML](https://flyanote.com/docs/email-format#errors-custom-html) * [Drafts from a model](https://flyanote.com/docs/email-format#errors-the-llm-profile) * [The brand kit](https://flyanote.com/docs/email-format#errors-the-brand-kit) * [Your account’s kit, images, and fonts](https://flyanote.com/docs/email-format#errors-the-context-validator) * [Liquid](https://flyanote.com/docs/email-format#errors-liquid) ### Structure The document’s shape: what goes where, what’s required, and what isn’t allowed. #### `document_too_large`: The document is too large Keep the whole document under 1 MB of JSON. Move long custom HTML into fewer sections, or split the email. #### `type_invalid`: A value has the wrong type Give this property the type the schema expects, such as a string, a number, an array, or an object. #### `property_required`: A required property is missing Add the property. The message names it. #### `property_unknown`: A property is not part of the format Remove the property, or check its spelling. Field names are snake\_case, such as content\_background. #### `not_supported_yet`: Not supported in this version yet Remove it. The name is reserved for a later version of the format. #### `value_invalid`: A value is not one of the allowed values Use one of the values the message lists. #### `schema_version_unsupported`: The schema version is not one this service reads Set schema\_version to the version your document was written for. The message names the current one. #### `string_too_short`: A string is too short Lengthen the string to at least the minimum the message gives, or remove the property if it is optional. #### `string_too_long`: A string is too long Shorten the string to at most the maximum the message gives. #### `array_too_short`: A list has too few items Add items until the list has at least the minimum the message gives. #### `array_too_long`: A list has too many items Remove items until the list has at most the maximum the message gives. #### `number_out_of_range`: A number is out of range Use a number inside the range the message gives. #### `number_not_integer`: A number must be whole Use a whole number, with no decimal part. ### Types of element A section, block, or span with a `type` or `kind` the format doesn’t have. #### `section_kind_unknown`: Unknown section kind Use one of the section kinds the message lists: columns, grid, hero, or html. #### `block_type_unknown`: Unknown block type Use one of the block types the message lists. #### `block_not_allowed_here`: This block type cannot go here Move the block into a column, or use a block type this container allows. The message lists them. #### `line_kind_unknown`: Unknown line kind Use one of p, h1, h2, h3, bullet, number, or quote. ### Values A color, size, or other value that isn’t written the way the format takes it. #### `color_invalid`: Not a color Use a palette token such as brand.primary or text.default, or an exact color as #rrggbb. Alpha and color names are not supported. #### `hex_invalid`: Not an exact color Dark overrides take an exact color only, as #rrggbb. #### `dimension_invalid`: Not a valid size Use a token such as space.md or radius.md, or whole pixels such as 16px, inside the range the message gives. #### `font_size_invalid`: Not a valid font size Use a size token such as size.body or size.h2, or whole pixels inside the range the message gives. #### `font_invalid`: Not a valid font Use font.heading or font.body, or the name of a font family from the catalog, such as Georgia. #### `button_width_invalid`: Not a valid button width Use auto, full, or whole pixels from 1px to 700px. #### `palette_invalid`: Not a valid palette id Use the id of one of the kit’s palettes, such as main: lowercase letters, digits, and underscores, but not kit, which is reserved. #### `theme_invalid`: Not a valid theme id Use light, dark, or the id of one of the palette’s themes: lowercase letters, digits, and underscores. #### `brand_invalid`: Not a valid brand id Use the id of one of the account’s brands as the API gives it (kit\_ and 26 lowercase letters and digits), or remove settings.brand to use the default brand. #### `lang_invalid`: Not a language tag Use a BCP 47 language tag, such as en or pt-BR. #### `timestamp_invalid`: Not a UTC timestamp Use an ISO 8601 timestamp in UTC, ending in Z, such as 2026-10-06T14:02:11Z. #### `pointer_invalid`: Not a JSON Pointer Use a JSON Pointer relative to the element, such as /alt. #### `currency_invalid`: Not a currency Use an ISO 4217 code such as USD, or a path to one in the data, such as data.order.currency. ### Links and sources Where a link goes and where an image comes from. #### `link_invalid`: Not a valid link Use a full address with its scheme: https://, mailto:, tel:, asset:, kit:, or system:. A link can also be a merge tag, such as {{ data.tracking\_url }}. #### `link_scheme_not_allowed`: This kind of link is not allowed Use an https://, mailto:, or tel: link instead. javascript:, data:, file:, and vbscript: links are never allowed. #### `link_relative`: A link has no scheme Write the full address, starting with https://. Relative links don’t work in email. #### `link_marketing_only`: This link is for marketing mail only Remove the unsubscribe or preferences link, or set meta.category to marketing. Transactional mail has no unsubscribe. #### `src_invalid`: Not a valid image source Use an uploaded image (asset:), a full web address (https:// is best), or a merge tag such as {{ data.image\_url }}. ### Text and merge tags The text model: lines, spans, marks, and the merge tags inside them. #### `span_text_liquid`: Merge tags in text must be field spans Replace the tag with a field span, such as { "field": "contact.first\_name", "fallback": "friend" }. Liquid tags ({% %}) are allowed only in custom HTML. #### `liquid_tag_not_allowed`: Liquid tags are not allowed here Use only output tags such as {{ data.order.number }} here. Tags like {% if %} are allowed only in custom HTML. #### `liquid_not_allowed`: Liquid is not allowed here Remove the {{ }} or {% %}. This field is plain text. #### `path_invalid`: Not a valid data path Write a path such as contact.first\_name or data.order.total: a namespace, then names joined by dots, with no brackets. #### `path_case_invalid`: A name under contact, account, message, campaign, links, or brand has a capital letter Write names under our namespaces in lowercase, as contact.first\_name, in a field and in Liquid alike. Names under data, and under a repeat’s variable, may use any case. #### `path_root_unknown`: A path starts with an unknown name Start the path with contact, account, message, campaign, data, links, or now, or with the variable of a repeat that contains it. #### `loop_variable_reserved`: A loop variable shadows a namespace Pick another name, such as item. contact, account, message, campaign, data, links, now, brand, and forloop are taken. #### `field_currency_without_money`: A currency is set on a field that isn’t money Set format to money, or remove the currency. #### `field_money_without_currency`: A money field has no currency Set currency to an ISO 4217 code such as USD, or a path to one in the data, such as data.order.currency. ### Rules across properties Rules that look at more than one property at once, such as unique ids and the footer. #### `marks_conflict`: Superscript and subscript together Keep either sup or sub on this span, not both. #### `line_level_orphan`: A nested list line has no list above it Put a bullet or number line before it, or remove level: 2. #### `line_start_nested`: A nested list line cannot restart numbering Remove start from this line. Only a top-level number line can start a new list. #### `widths_invalid`: Column widths don’t fit the columns Give one width per column, in whole percents that add up to 100, or remove widths for equal columns. #### `table_cells_mismatch`: A table row has the wrong number of cells Give every row, and the header, one cell per table column. Use an empty cell ({ "spans": \[\] }) for a blank. #### `image_alt_missing`: An image has no alt text Describe the image in alt, or set decorative: true if it carries no meaning. #### `image_alt_conflict`: An image has alt text and is marked decorative Remove alt from a decorative image, or remove decorative: true. #### `image_width_required`: An image from outside needs a width Set width, such as 300px. Images from an address or a merge tag can’t be measured when the email is built. #### `background_fallback_missing`: A background image has no fallback color Set background as well. It shows when images are off, and it’s what contrast is checked against. #### `background_image_size_required`: A hero background from outside needs its size Set background\_image.width and height in pixels, or use an uploaded image (asset:). #### `id_invalid`: Not a valid id Use the element’s prefix (sec\_, col\_, itm\_, or blk\_) and 8 lowercase Crockford base32 characters, or leave the id out and let the API fill it in. #### `id_duplicate`: Two elements share an id Give the copy a new id, or leave its id out and let the API fill one in. #### `kit_version_without_brand`: A kit version is pinned without its brand Set settings.brand to the kit the version belongs to, or remove settings.kit\_version to follow the kit’s newest version. #### `footer_duplicate`: More than one footer Keep one footer block, in the last section. #### `footer_not_last`: The footer isn’t in the last section Move the footer block into the last section. ### Custom HTML HTML you write yourself, in an `html` block or section, checked when it’s saved. The message gives the line and column inside the HTML. #### `html_parse`: The custom HTML can’t be read as part of an email Write the HTML that goes inside : no , , , or doctype, no NUL or other character HTML doesn’t allow, nothing nested over 100 deep, no tag with over 100 attributes, and no formatting tag such as or left open. #### `html_unsafe`: The custom HTML holds something email can’t safely carry Remove it, or use what the message suggests instead: a link instead of a form or frame, an instead of a CSS or background image, plain HTML instead of SVG. #### `html_style`: The custom HTML has a