# Flomailr documentation Source: https://flomailr.com/docs. Connector: https://flomailr.com/api/mcp. --- # Introduction Flomailr is an email marketing platform. You build a list of contacts, design emails, send them as campaigns, and set up automations that send on their own. You can do the design work by hand in a visual builder, or ask your own AI to do it through a connector. ## What is in the dashboard The sidebar has six entries. Several of them group related pages as tabs. | Section | What it holds | | ------------- | ------------------------------------------------------------------------------------ | | Overview | Sending results across your workspace, with a 7, 30 or 90 day view | | Campaigns | Your emails, plus tabs for RSS, SMS, templates, media and sponsors | | Automations | Visual flows that send emails and act on contacts when something happens | | Audience | Contacts, lists, segments and deals | | Pages & Forms | Landing pages and signup forms that add subscribers | | Settings | Your workspace, brand, sending health, guardrails, team, API keys and connected apps | ## Workspaces Everything you create lives in a workspace, which is one business. Contacts, lists, campaigns and settings never cross between workspaces. One login can belong to several workspaces, and each person has a role in each one. See [Settings](/docs/platform/settings). ## Design with your own AI Flomailr has a hosted connector for AI apps such as Claude. You describe the email, your AI writes the content and picks the sections, and Flomailr handles layout, typography and colour using your brand kit. Designs the AI saves are drafts, and every send starts as a dry run that you confirm. - [Connect Your AI](/docs/ai/connect) - [Designing Emails with AI](/docs/ai/design-emails) - [Sending Safely with AI](/docs/ai/send-safely) - [MCP Tool Reference](/docs/ai/tools) ## Build on it with the API A REST API covers contacts, lists, campaigns, automations and forms, plus events and transactional email. See the [API Overview](/docs/api/overview). ## Where to go next - New here: [Quick Start](/docs/quick-start) - How the pieces work: the Platform Guide, starting with [Contacts](/docs/platform/contacts) - Why a send was held back or an email landed in spam: [Sending Health](/docs/concepts/sending-health) and [Compliance](/docs/concepts/compliance) --- # Quick Start This walks you from a new account to a sent campaign. It takes a few minutes of setup, plus however long the email takes you to write. ## 1. Create your account and workspace 1. Sign up and confirm your email address. You cannot reach the dashboard until you do. 2. On the `Set up your workspace` screen, enter a workspace name (2 to 80 characters). It is the name of your team or business, and you can change it later. ## 2. Set your sending details Open `Settings > Workspace` and fill in the `Email sending` card: - **From name** and **reply-to**, which control how your emails appear in an inbox. - **Business mailing address.** This is required before you can send a campaign. It appears in every email footer, as marketing email rules require. To send from your own domain instead of the shared Flomailr address, add a custom domain on the same page and publish the DNS records it shows you. See [Sending Health](/docs/concepts/sending-health). ## 3. Add contacts Go to `Audience > Contacts`. - `Add contact` creates one person. - `Import CSV` loads a file. It needs an `email` column. First name, last name, tags and status columns are recognized, and the importer accepts common header spellings. The limits are 5 MB and 10,000 rows per file. Only contacts with the status `SUBSCRIBED` receive campaigns. See [Contacts](/docs/platform/contacts) and [Double Opt-In](/docs/concepts/double-opt-in). To collect subscribers instead of importing them, build a form or a landing page under `Pages & Forms`. See [Forms](/docs/platform/forms). ## 4. Make a list Go to `Audience > Lists`, choose `New list`, then add contacts to it. A campaign is sent to a list. See [Lists](/docs/platform/lists). ## 5. Create a campaign Go to `Campaigns` and choose `New campaign`. You have two ways to design it: - **By hand.** Use the visual builder. Drag in blocks such as headings, text, images and buttons, and edit text directly on the canvas. See [Campaigns](/docs/platform/campaigns). - **With your AI.** Connect Claude or another AI app, ask it for the email, and open the draft it saves in the builder to adjust. See [Connect Your AI](/docs/ai/connect) and [Designing Emails with AI](/docs/ai/design-emails). Choose the list the campaign goes to, and set the subject line. Merge tags such as `{{firstName}}` are filled in per recipient. See [Personalization](/docs/concepts/personalization). ## 6. Test, then send - Send yourself a test email from the builder first. - Send the campaign now, or schedule it for later. Before a campaign goes out, Flomailr skips suppressed addresses and applies your guardrails (daily cap, quiet hours, blocked domains) and your plan's monthly limit. Each email gets an unsubscribe link automatically. See [Compliance](/docs/concepts/compliance). ## 7. Check the results Open `Overview` for results across your workspace, or a campaign's own analytics page for one send. See [Analytics](/docs/platform/analytics). ## Next steps - Automate a welcome series: [Automations](/docs/platform/automations) - Connect your own app: [API Overview](/docs/api/overview) --- # Connect Your AI Flomailr runs a hosted connector that speaks the Model Context Protocol (MCP). Once your AI app is connected, you can ask it to design emails in your brand, preview them, save drafts, and send, using your own Flomailr workspace. Flomailr does the layout, typography and colour. Your AI writes the words and picks the sections. The connector URL is the same for everyone: ```text https://flomailr.com/api/mcp ``` `Settings > Connect your AI` in the dashboard shows this page's instructions with copy buttons. ## Claude on the web and in the desktop app 1. In Claude, open `Settings`, then `Connectors`. 2. Choose `Add custom connector`, name it Flomailr, and paste `https://flomailr.com/api/mcp`. 3. Claude opens Flomailr's approval screen. Sign in if asked, check the workspace name shown on the screen, and click `Allow access`. 4. Start a chat with the Flomailr connector turned on and ask for an email. The first time Claude shows an email preview, it asks whether to allow it. Click `Allow` to see the rendered email, with desktop, mobile and dark mode views, inside the chat. This route uses OAuth. You sign in with your own Flomailr login, and the connector acts in the workspace you approved. ## Claude Code Add the connector from your terminal: ```bash claude mcp add --transport http flomailr https://flomailr.com/api/mcp ``` Start Claude Code, run `/mcp`, pick `flomailr` and sign in. Your browser opens the same Flomailr approval screen. To use an API key instead of signing in, pass it as a header. An admin creates the key under `Settings > API keys` (see [API Keys](/docs/api/api-keys)): ```bash claude mcp add --transport http flomailr https://flomailr.com/api/mcp \ --header "Authorization: Bearer " ``` ## Cursor Not yet verified by Flomailr. Cursor supports remote MCP servers through its MCP settings. Add a server named `flomailr` with the URL above. If Cursor offers to sign in, approve access on the Flomailr screen. If it asks for a header instead, send `Authorization: Bearer `. If a step does not match what you see, email hello@flomailr.com and say what happened. ## ChatGPT Not yet verified by Flomailr. ChatGPT can add custom connectors in developer mode. Add a custom connector with the URL above, choose OAuth if it asks how to sign in, and approve access on the Flomailr screen. If a step does not match what you see or the connection fails, email hello@flomailr.com. ## Sign in or use an API key | | Sign in (OAuth) | API key | | --------------------- | ------------------------------------------------------------------ | ---------------------------------------------- | | Who approves | You, with your own Flomailr login | A workspace admin creates the key | | Workspaces | Can list and switch between the workspaces your account belongs to | Locked to the one workspace the key belongs to | | Brand and suppression | Only owners and admins can change them, as in the dashboard | Allowed, because keys are created by admins | | Revoke | `Settings > Connected apps` | `Settings > API keys` | Over OAuth the connector offers 34 tools. With an API key it offers 32, without `list_workspaces` and `switch_workspace`. See the [MCP Tool Reference](/docs/ai/tools). Each workspace is a separate business. Contacts, lists and campaigns never cross between them. If you run several, name the business in your request and the AI will switch to it. ## Disconnect Open `Settings > Connected apps` and click `Disconnect` next to the app. It stops working immediately and would have to ask for your approval again to reconnect. Members see the apps they approved. Owners and admins see every connection in the workspace. To cut off a key, revoke it under `Settings > API keys`. ## See what the AI did Every action the connector takes, such as saving a draft, a dry run, a send, a suppression change or a brand update, is written to an append-only log. Owners and admins can read it under `Settings > Activity`, and you can ask your AI to read it with the `get_action_log` tool. Revoking a key or disconnecting an app does not erase the log. ## What to ask first - "Import our brand from https://yourwebsite.com and show me the logo, colors and fonts you picked up." - "Write this month's newsletter in our brand, with these three posts as article cards and a button to the shop. Show me a preview." - "Send the 'Spring sale' draft to the Customers list. Do a dry run first and tell me who would get it and who would be skipped." More in [Designing Emails with AI](/docs/ai/design-emails) and [Sending Safely with AI](/docs/ai/send-safely). ## For agents and developers Flomailr publishes machine-readable descriptions of itself, so AI agents and MCP clients can find and configure it without these instructions: - [llms.txt](https://flomailr.com/llms.txt) lists every docs page, and each page has a Markdown copy at the same address with `.md` on the end (for example [this page](https://flomailr.com/docs/ai/connect.md)). [llms-full.txt](https://flomailr.com/llms-full.txt) is all of it in one file. - The [MCP Server Card](https://flomailr.com/api/mcp/server-card) describes the connector and how to connect, and the [AI Catalog](https://flomailr.com/.well-known/ai-catalog.json) points domain-level discovery at it. - The [OpenAPI spec](https://flomailr.com/openapi.json) describes the REST API, for coding agents that call it directly instead of using MCP. --- # Designing Emails with AI When you ask a connected AI for an email, the work is split in two. The AI writes the content and chooses the sections. Flomailr owns layout, typography and colour, so the result follows a theme, uses your brand, and renders in the major inboxes including Outlook. Connect your AI first: [Connect Your AI](/docs/ai/connect). ## The workflow 1. **Read the design kit.** The AI calls `get_design_kit`, which returns the themes, the section patterns and your brand kit (if you have one). If there is no brand kit and you have a website, it should offer to import one. 2. **Compose.** The AI calls `compose_email` with an ordered list of sections and `save: { name, subject }`. Saving creates a draft campaign and returns its id. It never sends anything. 3. **Preview.** The AI calls `preview_email` with that campaign id. You see the real email in the chat, with desktop, mobile and dark inbox views, and the deliverability grade. 4. **Revise.** For small changes the AI uses `edit_campaign`. For a new look it uses `restyle_email`. Neither needs the whole design sent again. 5. **Finish in Flomailr.** The draft appears in `Campaigns` and opens in the visual builder, where you can adjust anything by hand, pick a list and send. Or ask your AI to send it with a dry run first (see [Sending Safely with AI](/docs/ai/send-safely)). The AI is told never to invent prices, statistics, quotes, dates or links. If it needs one, it should ask you. ## Themes A theme sets colours, fonts, spacing and corner shapes for the whole email. The AI picks one by its id. If you do not name a theme, your brand kit's base theme is used when you have one, and Clean otherwise. | Theme | Id | What it is for | | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------- | | Clean | `clean` | Crisp and neutral. Product updates, onboarding, SaaS, anything that should feel trustworthy and modern | | Editorial | `editorial` | A printed-magazine feel with serif headlines on paper tones. Newsletters, essays, long reads, publishers | | Studio | `studio` | Gallery-white, black type, uppercase tracked headings, square corners. Fashion, retail, design studios, architecture | | Hearth | `hearth` | Warm cream and terracotta with a friendly serif and pill buttons. Restaurants, cafes, bakeries, wellness, local shops | | Midnight | `midnight` | Dark, glowing, confident. Launches, tech, events, nightlife, anything that wants to feel like a keynote | | Poster | `poster` | Loud condensed headlines and a hot accent. Sales, drops, promos, sports, concerts, anything with a deadline | | Fresh | `fresh` | Airy mint and leaf green with soft corners. Health, food, eco, outdoors, fitness, nonprofits | | Mono | `mono` | Typewriter headings, plain layout, no decoration. Developer tools, changelogs, technical newsletters | | Luxe | `luxe` | Refined serif capitals, ivory and gold, generous space. Hospitality, jewelry, real estate, weddings, fine dining | Text and link colours in every theme are adjusted so they stay readable on the background they sit on. To compare looks, ask for the same email in two themes. ## Section patterns An email is an ordered list of sections, up to 20. A typical order is header, hero, one to four body sections, a call to action, then footer. Each section uses one of these patterns: | Pattern | What it does | | ------------- | ------------------------------------------------------------------------------------------------- | | `header` | Logo (from the brand kit when set) or wordmark, optional nav links. Usually first | | `hero` | The opening statement. With an image it becomes a photo hero, without one a bold typographic band | | `text` | A heading and Markdown body copy, optional button. The workhorse for letters and updates | | `features` | 2 to 4 benefits or items, as columns (short copy) or a list (longer copy, optional thumbnails) | | `products` | 1 to 4 product cards with image, price, optional compare-at price and a buy button | | `articles` | 1 to 6 article or blog teasers with optional thumbnails. Newsletters and digests | | `testimonial` | One customer quote with name, role and optional avatar and star rating | | `stats` | 2 to 4 big numbers with labels. Only real figures | | `cta` | A focused closing call to action on a contrasting band | | `event` | Event details (what, when, where) with an RSVP button and optional add-to-calendar links | | `image` | A single image with alt text, optional link and caption | | `countdown` | A live countdown to a deadline with an optional button | | `table` | A simple data table (schedule, pricing, order summary). The first row is the header | | `signoff` | A personal sign-off: closing line, name, title, optional headshot | | `footer` | Brand name, links and social icons. The unsubscribe link and postal address are added at send | | `divider` | A thin rule between sections | | `spacer` | Vertical breathing room | Body copy is written in Markdown (bold, italic, links, bullet lists). Links must be http, https, mailto or tel. Images must be public https URLs. If you have a file instead of a URL, the AI can upload it with `upload_asset` (PNG, JPEG, GIF or WebP up to 5 MB; SVG is refused) and use the permanent URL it returns. The footer pattern does not need your unsubscribe link or address. Flomailr adds both when the campaign is sent. A campaign send is blocked until you add a business mailing address under `Settings > Workspace`, in the Email sending card. ## Brand kits A brand kit holds your logo, colours, fonts, voice and social links. When one exists, every composed email uses it, layered over the theme. Set it up in either place: - **Settings > Brand.** Enter your website and click `Import from website`. Flomailr reads your homepage for a logo, colours, fonts and social links and fills the form. Nothing is saved until you press `Save`. You can also fill the fields yourself. Only the primary colour is required. Saving the brand needs an owner or admin role. - **Through your AI.** Ask it to import your brand from your URL. The `import_brand_from_url` tool returns a proposal with logo candidates and where each value came from, and saves nothing unless asked to. The AI should show you the proposal, let you correct it, then save it with `set_brand`. Fonts found on a website are mapped to email-safe equivalents. The voice field is free text, for example "warm, plain-spoken, no exclamation marks". The AI is told to write in it. ## Changing a draft - `get_campaign` returns an outline of the draft: each block's id, type and a one-line summary. Use `format: "full"` for the whole design. - `edit_campaign` applies edits by block id. The operations are `update` (merge new values into a block), `remove`, `move`, `insert` (a themed section or a raw block, placed at, after or before a position), `settings` (such as the preview line) and `restyle` (a new theme). Up to 50 operations run in order and either all apply or none do. It can also change the subject and name, and it returns the new outline and audit. - `restyle_email` reassigns every colour, font, size and corner radius by role from a theme and your brand, and leaves content and structure alone. Use it for "make this match our brand" or to try another look. It also repairs low-contrast text. Only drafts can be edited by the connector. A campaign that is scheduled, sending or sent is refused. ## Checks that run on every design Every compose, edit and preview returns a deliverability and accessibility audit with an A to F grade and findings. The audit covers deliverability, accessibility, spam, content, structure and design. The design checks look at text contrast against the real background, too many fonts, heading order, and leftover sample copy. The same findings appear in the builder's preflight panel. Ask your AI to fix every error-level finding before sending. ## Preview notes The preview fills merge tags with a sample contact, Alex Morgan. Images hosted outside Flomailr may not load inside the preview, and the real email is not affected. If the preview does not appear in your AI app, the tool also returns a link to open the draft in the builder. ## Prompts that work well - "Import our brand from https://example.com and show me the logo, colors and fonts you picked up." - "Write this month's newsletter in our brand: a short intro, the three blog posts I'm pasting below as article cards, and a button to the shop. Show me a preview." - "Make an email for our open house on Saturday, 10am to 2pm, with an RSVP button. Try it in the Editorial and Hearth themes so I can compare." - "Open the 'Spring sale' draft, change the button text to 'Shop the sale', then check it for deliverability and accessibility problems." - "Restyle the 'Spring sale' draft in the Midnight theme and keep the copy the same." - "Add a testimonial section after the features in the 'Welcome' draft. The quote is: ..." Give the AI the real details: dates, prices, links and image URLs. The more specific the brief, the fewer questions it has to ask. ## Related - [Connect Your AI](/docs/ai/connect) - [Sending Safely with AI](/docs/ai/send-safely) - [MCP Tool Reference](/docs/ai/tools) - [Campaigns](/docs/platform/campaigns) --- # Sending Safely with AI An AI can only send through Flomailr's own send path, and that path enforces its rules on the server. None of them are settings the AI passes in. The AI can ask for a send. It cannot talk its way past the checks. There are two send tools: - `send_email`: a personal or one-to-one message to 1 to 5 addresses. Not marketing. - `send_campaign`: an existing campaign, sent to its list. Both start as a dry run. ## Dry run, then confirm Both tools default to `dryRun: true`. A dry run sends nothing and reports what would happen: - `send_campaign` returns the audience counts (total, allowed, blocked, deferred), the blocked addresses with the reason for each, the deferred addresses with the time they can go, and how much of today's daily cap is left. Counts are exact. The address lists show up to 25 of each, so a large list does not flood the chat. - `send_email` returns the exact From address, the subject, the size of the rendered HTML, who would receive it and who is blocked. To send for real the AI must call again with `dryRun: false` and `confirm: true`. With `dryRun: false` and no `confirm`, nothing is sent and the result is `BLOCKED`. Show yourself the dry run before you agree to the second call. You can force dry runs. In `Settings > Guardrails`, turn on `Agents must opt in to real sends`. While it is on, the send tools report a dry run even when the AI asks for a real send. ## What the server checks | Check | What it does | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Suppression list | Addresses that hard bounced, complained, unsubscribed or were blocked by hand are skipped. `send_email` always skips them. For `send_campaign` the check is on by default and the send also drops suppressed addresses on its own. Blocked reason: `SUPPRESSED` | | Blocked domains | Any recipient on a domain in your never-send list is skipped. Reason: `BLOCKED_DOMAIN` | | Daily send cap | An optional ceiling on messages per calendar day, counted in your default time zone. Recipients past the cap are blocked. Reason: `DAILY_CAP`. Both send tools respect it | | Quiet hours | An optional window, such as 21:00 to 08:00, where `send_campaign` holds messages back. It uses each contact's own time zone and falls back to your default. Those recipients are deferred, not dropped. `send_email` does not use quiet hours | | Master switch | If `Enforce sending guardrails` is off, every guarded send is blocked rather than sent unchecked | | Suspension | A suspended workspace cannot send at all | | Monthly plan limit | A send that would pass your plan's monthly limit is refused | | Invalid addresses | Malformed addresses are blocked (`INVALID_EMAIL`) | `send_campaign` also needs the campaign to have a list with subscribed contacts, and your workspace needs a business mailing address, because campaign emails carry it in the footer. It sends list-based campaigns only. A campaign aimed at a segment must be sent from the dashboard. The AI tools sort recipients one by one, so a campaign can go out to some people while others are blocked or deferred. Sends started from the dashboard, the REST API or a schedule are stricter: a send that would pass the daily cap, or that starts inside quiet hours, is refused as a whole, and a scheduled one is retried after the window ends. Admins set the cap, quiet hours, blocked domains and switches under `Settings > Guardrails`. Members can read them. See [Sending Health](/docs/concepts/sending-health). ## The 5-recipient cap `send_email` accepts at most 5 recipients. This is a hard limit in the server. A request with 6 or more is refused, and so is a request with any invalid address, so nothing is silently dropped. Duplicates are removed. `send_email` adds no tracking pixel and no marketing unsubscribe footer, because it is meant for a personal note. The message can be a designed email (the AI passes the design and Flomailr renders it), your own HTML, or plain text. If the rendered HTML is larger than 102 KB, the result warns that Gmail will clip it. For anything bigger than five people, the AI should save a campaign and use `send_campaign`. ## Idempotency Both tools take an `idempotencyKey`. The AI should pass a stable one on any real send. - Calling again with the same key and the same request returns the original result, marked `REPLAYED`, and sends nothing. - The same key with a different request is rejected. - The same key while the first call is still running returns `BLOCKED`. Campaign sends have a second layer. Recipients go out in batches of 100, and each accepted batch is recorded before the next one starts. If a large send is cut short, it continues in the background and finishes by itself. The result then says mode `SENDING`. The AI should not call send again, and if it does, the repeat only resumes the same send and never mails anyone twice. ## Quiet hours override `send_campaign` has an `overrideQuietHours` option, which lets a real send ignore your quiet-hours window. It affects quiet hours only. It does not change suppression, blocked domains, the daily cap, suspension or the monthly limit. If you do not want a send to go out at night, ask your AI not to use it. ## Everything is logged Each action goes into an audit log: dry runs, sends, direct sends, saved drafts, suppression changes, policy reads, readiness and compliance checks, performance reads and brand updates. Each entry records the tool, a plain summary, the campaign and the idempotency key. - Owners and admins read it under `Settings > Activity`, filtered by action. - Your AI can read it with `get_action_log`. The log is append-only. Revoking a key or disconnecting an app does not erase what it did. ## Who can change what When the connector is signed in over OAuth, it acts as you. Adding or removing suppression entries, and saving the brand kit, need an owner or admin role. Suppression changes also need `confirm: true`. Connected through an API key, those actions are allowed, because only admins can create keys. Guardrail settings themselves can only be edited in the dashboard, not through the connector. ## A safe order for the AI 1. `get_send_policy` to read the cap, quiet hours, blocked domains and how many sends are left today. 2. `check_send_readiness` for a go, caution or no-go on your sending domain and recent bounce and complaint rates. 3. `check_compliance` and `score_email` on the draft. Fix errors. 4. `send_campaign` with `dryRun: true`. Read the numbers and the blocked list. 5. After you agree, `send_campaign` with `dryRun: false`, `confirm: true` and an `idempotencyKey`. 6. `get_campaign_performance` afterwards to see opens, clicks, bounces and complaints. ## Related - [Connect Your AI](/docs/ai/connect) - [MCP Tool Reference](/docs/ai/tools) - [Compliance](/docs/concepts/compliance) --- # MCP Tool Reference The hosted connector at `https://flomailr.com/api/mcp` offers 35 tools when you sign in over OAuth, and 33 when you connect with an API key (the two workspace tools are left out). Your AI app chooses the tools. You normally never name them yourself, but this page shows what exists and what each one needs. See [Connect Your AI](/docs/ai/connect) to set up the connection. A design is a Flomailr `BlockDoc`, a JSON document `{ version: 1, settings, blocks }`. Where a tool takes `doc`, it accepts the object or a JSON string. You rarely need to write one by hand: `compose_email` builds it from sections. Actions such as saving drafts, sending, suppression changes and brand updates are written to the activity log. See [Sending Safely with AI](/docs/ai/send-safely). ## Design | Tool | What it does | Key inputs | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `get_design_kit` | Returns the themes, the section patterns and your brand kit. Read-only. Call it first when designing | none | | `compose_email` | Builds a complete email from an ordered list of sections, using a theme and your brand kit. With `save` it creates a draft and returns its id | `sections` (1 to 20), `theme`, `useBrand`, `preheader`, `save: { name, subject, campaignId? }` | | `restyle_email` | Re-skins a design into a theme or your brand without changing content or structure. With `campaignId` it updates the draft | `campaignId` or `doc`, `theme`, `useBrand` | | `preview_email` | Shows the real rendered email in the chat with desktop, mobile and dark views, plus the audit grade and problems. Read-only | `campaignId` (preferred) or `doc`, `subject` | | `render_email_preview` | Supplies the HTML to the preview view. Used by the view itself, not by agents | `campaignId` or `doc`, `subject` | | `screenshot_email` | Renders the email in a real browser and returns screenshots the AI can look at, to check the design itself. Read-only | `campaignId` (preferred) or `doc`, `device` (`desktop` or `mobile`), `maxFrames` (1 to 4) | | `get_campaign` | Loads a campaign. The default outline lists each block's id, type and a one-line summary. Read-only | `campaignId`, `format` (`outline` or `full`) | | `edit_campaign` | Applies small edits to a draft by block id, all or nothing, and returns the new outline and audit | `campaignId`, `ops` (up to 50), `subject`, `name` | `compose_email` sections use these patterns: `header`, `hero`, `text`, `features`, `products`, `articles`, `testimonial`, `stats`, `cta`, `event`, `image`, `countdown`, `table`, `signoff`, `footer`, `divider`, `spacer`. Themes: `clean`, `editorial`, `studio`, `hearth`, `midnight`, `poster`, `fresh`, `mono`, `luxe`. Details are in [Designing Emails with AI](/docs/ai/design-emails). `edit_campaign` operations: `update`, `remove`, `move`, `insert`, `settings`, `restyle`. ## Brand | Tool | What it does | Key inputs | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `get_brand` | Returns the workspace brand kit, or null. Read-only | none | | `set_brand` | Replaces the brand kit. Owners and admins only | `colors` (`primary` required), `name`, `websiteUrl`, `logoUrl`, `logoWidth` (40 to 600), `headingFont`, `bodyFont`, `baseTheme`, `voice`, `socials` (up to 10) | | `import_brand_from_url` | Reads a website and proposes a brand kit with logo candidates and the evidence for each value. Saves only if asked, and saving needs an owner or admin | `url`, `save` (default false) | ## Campaigns and content | Tool | What it does | Key inputs | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `save_campaign` | Saves a `BlockDoc` as a draft campaign. Never sends or schedules. Returns the campaign id, a builder link and a grade | `name`, `subject`, `doc`, `campaignId` (to update a draft) | | `list_campaigns` | Lists campaigns, most recent first | `status` (`DRAFT`, `SCHEDULED`, `SENDING`, `SENT`), `limit` (default 20, up to 100) | | `import_html_campaign` | Saves finished HTML as a draft with the markup stored exactly as given. Does not send | `html`, `name`, `subject`, `text`, `campaignId` | | `check_links` | Fetches every link and image in some HTML or a stored campaign and reports the HTTP status, to catch dead links and blocked images | `html` or `campaignId` | | `upload_asset` | Uploads an image to the workspace library and returns a permanent public URL. PNG, JPEG, GIF or WebP up to 5 MB. SVG is refused | `filename`, `contentBase64` | | `list_assets` | Lists uploaded images with their URLs, so they can be reused | `query`, `limit` (default 30, up to 100) | Saving, composing and editing only ever touch drafts. A scheduled, sending or sent campaign cannot be changed through the connector. ## Drafting tools that need no account data These work on a design you give them and read nothing from your workspace. | Tool | What it does | Key inputs | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `render_email` | Renders a `BlockDoc` to the final email-safe HTML, using the same renderer as the app | `doc` | | `render_plaintext` | Renders a `BlockDoc` to a plain-text alternative | `doc` | | `audit_email` | Lints a design for deliverability, accessibility, spam risk and content. Returns a 0 to 100 score, an A to F grade and findings | `doc`, `subject` | | `preview_personalization` | Fills merge tags from a sample contact and reports which tags resolved, were empty or were unknown | `doc`, `contact` (`email`, `firstName`, `lastName`), `subject` | | `scaffold_email` | Builds a valid `BlockDoc` from a compact list of block specs, for when no section pattern fits | `blocks`, `settings` | | `list_block_types` | Lists every block type with its default props | none | ## Workspaces (OAuth only) | Tool | What it does | Key inputs | | ------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `list_workspaces` | Lists the workspaces your account belongs to, with contact, list and campaign counts, and which one is active | none | | `switch_workspace` | Points the connector at another workspace. It does not change the workspace open in your browser | `workspace` (id or exact name) | ## Sending and safety checks | Tool | What it does | Key inputs | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `get_send_policy` | Returns the server-enforced rules: the daily cap and what is left, blocked domains, the suppression check, quiet hours, and whether dry run is forced | none | | `check_send_readiness` | A go, caution or no-go report on domain authentication (SPF, DKIM, DMARC), the sending provider's account health, recent bounce and complaint rates against the suspend thresholds, and monthly quota | `domain` (defaults to your custom domain) | | `score_email` | Scores spam and compliance risk of a draft: trigger phrases, image-to-text ratio, link count, subject problems, unsubscribe handling | `doc`, `subject`, `hasListUnsubscribe` | | `check_compliance` | Checks a campaign or draft for CAN-SPAM and GDPR readiness: unsubscribe, postal address, sender identity and, with a list, recorded consent | `campaignId`, or `doc` and `subject`, plus `physicalAddress`, `listId` | | `send_campaign` | Sends an existing campaign to its list through the guarded pipeline. Dry run by default | `campaignId`, `dryRun` (default true), `confirm`, `overrideQuietHours`, `idempotencyKey` | | `send_email` | Sends a personal or one-to-one email to 1 to 5 recipients. Dry run by default | `to`, `subject`, one of `doc`, `html` or `body`, `replyTo`, `dryRun` (default true), `confirm`, `idempotencyKey` | How the guards work is on [Sending Safely with AI](/docs/ai/send-safely). ## Audience and results | Tool | What it does | Key inputs | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `get_campaign_performance` | Returns a sent campaign's sends, opens, clicks, bounces, complaints and unsubscribes with rates, any A/B split, and a suggested next step | `campaignId` | | `get_recipient_engagement` | Returns a group of recipients (hard bounced, complained, clicked, or did not open) with suggested suppressions | `cohort` (`bounced`, `complained`, `unengaged`, `clicked`, `all`), `campaignId`, `limit` (default 100, up to 500) | | `manage_suppression_list` | Lists or checks the suppression list, or adds or removes addresses. Adding and removing need `confirm: true` and an owner or admin over OAuth | `action` (`list`, `check`, `add`, `remove`), `emails`, `reason`, `note`, `confirm`, `idempotencyKey` | | `get_action_log` | Returns the audit trail of tool actions, filterable by action, campaign and time | `action`, `campaignId`, `since` (ISO time), `limit` (default 50, up to 200) | `get_recipient_engagement` needs a `campaignId` for the `clicked` and `unengaged` groups. Suppression reasons are `MANUAL`, `HARD_BOUNCE`, `COMPLAINT` and `GLOBAL_UNSUBSCRIBE`, and `MANUAL` is the default. ## Prompt The connector also offers one prompt, `design_email`. It takes a brief and walks the AI through the design workflow: read the design kit, pick a theme, compose, preview, and fix any errors. It tells the AI not to send anything without your explicit go-ahead. ## The email preview `preview_email` shows the email inside the chat as an interactive view. Your AI app has to support interactive MCP views for it to appear. Claude does, and asks you to allow it the first time. If a client cannot show it, the tool also returns a link that opens the draft in the visual builder. --- # Contacts Contacts are the people you can email. You manage them under Audience > Contacts, and every contact belongs to one workspace. ## Add contacts - **By hand:** Audience > Contacts > Add contact. Email address is required. First name, last name, tags (comma-separated) and any custom fields are optional. If the email already exists, that contact is updated instead of duplicated, and its tags and custom fields are replaced by what you enter. - **From a file:** Audience > Contacts > Import CSV (see below). - **From a form:** people who sign up through a [Form](/docs/platform/forms) or a list's subscribe page become contacts. - **From code:** the [Contacts API](/docs/api/contacts). ## Import a CSV The file needs a header row. Only `email` is required. - Recognized columns are `email`, `firstName`, `lastName`, `tags` and `status`. Common variants such as "Email Address" or "First Name" are mapped for you. Separate tags inside a cell with commas or semicolons. - Columns that match one of your custom fields (by key or label, ignoring case and spaces) are imported. Any other column is skipped, and the result message lists it. - **Add to list** puts the imported contacts on a list (Unsubscribed and Bounced rows are left off). **Tag every imported contact** adds one tag to the whole batch, alongside any tags in the file. - Contacts are matched by email. An existing contact is updated, and a column your file leaves out keeps its current value. A row with tags replaces that contact's tags. - A `status` column marks rows as unsubscribed, bounced or complained. Those addresses are imported as unmailable and suppressed. An import never resubscribes someone who opted out. - Limits: 5 MB per file and 10,000 rows. Rows with a missing or invalid email are skipped and counted. ## Statuses | Status | Meaning | | ------------ | ---------------------------------------------------------- | | Subscribed | Can be emailed. | | Pending | Signed up through double opt-in and has not confirmed yet. | | Unsubscribed | Opted out. | | Bounced | The address hard-bounced. | Campaigns reach Subscribed contacts only. A Pending contact receives nothing until they confirm. See [Double Opt-In](/docs/concepts/double-opt-in). The contacts table opens on **Active**, which shows Subscribed and Pending. Unsubscribed and Bounced contacts are kept but hidden, with a "hidden, show all" link next to the status chips. Marking a contact Unsubscribed or Bounced, one at a time or in bulk, asks you to confirm. It adds the address to the suppression list and takes the contact off every list. Setting the status back to Subscribed does not undo that: only the person can re-subscribe, through a signup form or the preference centre. See [Compliance](/docs/concepts/compliance). ## Find and filter The filter bar searches email and name, and filters by status, tag, "Opened campaign", "Clicked in campaign" and "On list". Export CSV downloads exactly the contacts in the current view. ## Bulk actions Tick rows to open the action bar: **Change status**, **Add tag**, **Remove tag**, **Add to list** and **Delete**. Ticking covers one page, so use **Select all N matching** to include every contact that matches the current filters, up to 10,000 at a time. - Add to list skips Unsubscribed and Bounced contacts. - Delete suppresses Unsubscribed and Bounced addresses first, so deleting a contact never erases an opt-out. ## Tags and custom fields A contact can have up to 50 tags, each up to 100 characters. Custom fields are defined under Settings > Workspace, in the Custom contact fields card (see [Settings](/docs/platform/settings)). Each has a label, a key and a type: Text, Number, Date or Yes/No. You can define up to 30. Keys start with a letter and use lowercase letters, digits and underscores. `email`, `firstName`, `lastName`, `tags` and `status` are reserved. Custom fields show on the contact form, in imports and exports, and as conditions in [Segments](/docs/platform/lists). For merge tags in email copy, see [Personalization](/docs/concepts/personalization). ## A contact's page Click an email to open the contact. You can edit details, change the subscription status, and see engagement, the consent log and campaign activity. Each campaign in the activity list links to that campaign's analytics. **Erase personal data (GDPR)** replaces the name, email and tags with anonymous placeholders and removes the contact from every list. Engagement history stays but can no longer be traced to a person. This cannot be undone. --- # Lists Lists and segments are two ways to group contacts, and a campaign is sent to one of them. Both live under Audience, next to Contacts. - A **list** is a fixed group. People are on it because you added them, or because they signed up through a form. - A **segment** is a rule. Contacts join and leave it on their own as their tags, activity or fields change. ## Lists Create one at Audience > Lists > New list. Click a list to open it. - **Members:** add people with the Add contact search (type at least two characters of an email), with **Add all N contacts to this list**, or with **Import into this list**, which opens the CSV import with that list preselected. **Remove** takes a person off the list without deleting the contact. - **Subscribe page:** every list has a public page at `/subscribe/`. **Copy subscribe link** copies it. - **List settings:** rename the list or delete it. Deleting never deletes contacts. Any unsent campaign, RSS campaign or [Form](/docs/platform/forms) that used the list is left with no list, and the confirmation names them. Unsubscribed and Bounced contacts cannot be added to a list. When a contact becomes unsubscribed or bounced they are removed from every list at once, and a nightly sweep catches anything that slipped through. If older data still has some, a banner on Audience > Contacts shows the count and a **Clean up now** button. See [Contacts](/docs/platform/contacts). A list can hold Pending contacts, but sends only reach Subscribed ones. ## Segments Create one at Audience > Segments > New segment, name it, then build the rule. Choose **Match all** or **Match any** of the conditions, add them with **+ Add condition**, and click **Preview count** to see how many subscribed contacts match the rules on screen, saved or not. Click **Save segment** to keep it. With no conditions, a segment matches every subscribed contact. A segment only ever contains Subscribed contacts. It is worked out again each time you preview it or send to it, so it is never a stale snapshot. | Condition | What it checks | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Subscription status | Is Subscribed (the only choice, since segments are subscribed contacts). | | Tag | Has tag, or does not have tag. | | List membership | Is in a list, or is not. | | Custom field | Operators depend on the field type: equals, contains, greater or less than, before or after a date, within the last N days, yes or no. Needs a custom field. | | Email engagement | Has, or has never, opened an email or clicked a link in the last 7, 14, 30, 60 or 90 days. Machine and Apple Mail Privacy Protection opens do not count. | | Lifecycle stage | Is, or is not: Active, Cooling off, Dormant, Never engaged, New. | | Lead score | Is at least, or at most, a number of points from Settings > Lead scoring. | | Churn risk | Is, or is not: low, medium, high. | | Paid membership | Is, or is not, a paying member (Settings > Paid tiers). | | Date added | Added before or after a date. The date is read as midnight in your browser's timezone. | See [Email Tracking](/docs/concepts/tracking) for how opens and clicks are recorded. Deleting a segment does not touch its contacts, but a campaign still using it loses its audience. ## How a campaign picks its audience In the campaign builder, open **Settings** and choose either **Send to list** or **Or send to segment**. The segment menu appears once you have at least one segment. A campaign has one audience, so picking one clears the other. When the campaign sends, Flomailr starts from the list's Subscribed members or the segment's matches, then drops: - addresses on the suppression list - contacts who opted out of the campaign's Topic, if you set one - anyone your Settings > Guardrails rules block The Send panel shows "Sending to" with the audience name. A list also shows its subscriber count. A segment's size is worked out at send time. See [Campaigns](/docs/platform/campaigns) for the rest of the send. --- # Campaigns A campaign is one email sent to a list or a segment. Find them under Campaigns > All campaigns. The other tabs in that section are RSS, SMS, Templates, Media and Sponsors. This page is about email campaigns. ## Create a campaign Click **New campaign** and pick a starting point: one of your saved templates, or Welcome, Newsletter, Announcement or Blank canvas. That creates a draft and opens the builder. Name it in the box at the top left. Your own AI can create drafts too. A design saved through the connector always arrives as a Draft. You send it from here, or ask your AI to send it, which starts as a dry run you confirm. See [Designing emails with AI](/docs/ai/design-emails) and [Connect your AI](/docs/ai/connect). The campaign list searches name and subject and filters by All, Draft, Scheduled or Sent. Each row has Edit (Analytics once sent), Duplicate, Save as template and Delete. A duplicate is a new draft named "(copy)" with the same design, audience, topic and A/B settings. ## The builder The palette on the left holds the blocks, the canvas is in the middle, and the inspector on the right edits whatever you select. - **Blocks** are grouped as Content (Heading, Text, Image, Button, List, Quote, HTML, Table), Media & social (Video, Banner, Gallery, Social, Menu), Conversion (Signup, Quote card, Timer, Product, Article, Poll, Calendar) and Layout (Hero, Layers, Columns, Section, Divider, Spacer). Drag a block onto the canvas, click it to add it at the end, or use the insert control between two blocks. **Search blocks** filters the palette. - **Text is edited on the canvas.** Click a block to open its other settings. With nothing selected, the inspector shows Email settings: preheader text, width, colors, link color and UTM tracking. - **Top bar:** Desktop and Mobile preview, a dark-mode preview, Undo and Redo, **History** (versions and review comments), **Outline** (Export HTML, and Import HTML, which adds your markup as one HTML block), **Check**, **Settings**, **Send** and **Save**. - **Check** grades the email A to F and lists findings as Must fix, Should fix or Consider. Click one to jump to the block. It is advice and does not block a send. - The **Plain Text** tab holds the version for clients that do not render HTML. The builder saves about two seconds after each change. Press Ctrl+S to save now, or `?` to list the shortcuts. Merge tags such as `{{firstName}}` are covered in [Personalization](/docs/concepts/personalization). ## Settings: subject, audience, A/B test Open **Settings** in the top bar for the subject line, **Send to list** or **Or send to segment** (see [Lists](/docs/platform/lists)), and **Topic** if you have set any up. Contacts who opted out of the topic are skipped. **A/B test the subject** splits the audience across two subject lines. Enter a Variant B subject and set the split with the slider, from 10% to 90%. If Variant B is blank, everyone gets the main subject. After the send, the campaign's analytics show results for A and B. **Declare winner** only records a label. It does not send anything further. ## Test, schedule and send Open **Send** in the top bar. - **Send yourself a test** emails the current builder content with "[TEST]" in the subject. It is limited to 10 test emails an hour per workspace. - **Schedule for later:** choose a date and time in your browser's timezone and click **Schedule send**. It must be at least 30 seconds ahead and needs an audience. The campaign shows as Scheduled, and **Cancel schedule** returns it to Draft. Due campaigns are picked up every minute. - **Send now** asks you to confirm, and a send cannot be undone. Large sends go out in batches of 100. If one does not finish in the first request, the campaign shows as Scheduled and the rest follows over the next minutes. Nobody who already received it is sent it again. A sent campaign cannot be edited, and a campaign cannot be deleted while it is sending. ## What blocks a send | Cause | What to do | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | No audience | Pick a list or segment in Settings. Send now and Schedule send stay disabled until you do. | | No mailing address | Add the Business mailing address under Settings > Workspace. It appears in every footer. See [Compliance](/docs/concepts/compliance). | | Guardrails | Nothing sends while guardrails are switched off in Settings > Guardrails. Quiet hours, a daily cap and blocked domains also apply. | | Monthly sending limit | The error says how many sends remain this month. See [Sending Health](/docs/concepts/sending-health). | | Suspended workspace | Sending stays off until support reinstates it. | | Nobody eligible | The audience has no Subscribed contacts, or all are suppressed or opted out of the topic. | If a scheduled campaign fails for a reason that needs a person (no mailing address, over a limit, nobody eligible), it goes back to Draft and the reason is recorded in the Sending audit log under Settings > Sending health. A quiet-hours block is retried after the window ends. See [Sending safely with AI](/docs/ai/send-safely) for how the same checks apply to AI sends. ## Templates Click the Save as template button on a campaign row, name it, and add an optional description. It appears under Campaigns > Templates and on the starting-point screen, and it keeps the subject line. Deleting a template does not change campaigns already made from it. ## Analytics A sent campaign's row links to its analytics: sent, human open rate, click rate, bounce rate, unsubscribed, A/B results, and an Export CSV button. See [Analytics](/docs/platform/analytics) and [Email Tracking](/docs/concepts/tracking). --- # Automations An automation is a flow that runs on its own. A trigger enrolls a contact, and the contact moves through steps such as emails, waits and branches. Open Automations in the sidebar to see them. Each card shows a status and how many contacts are enrolled. **New automation** creates a draft with a trigger and opens the builder. ## The builder The flow is a canvas of steps joined by lines. Click a step to edit it in the panel on the right. Use the plus button under a step (**Add a step**) to add an Action, Wait, Condition or End. Changes save about two seconds after you make them. | Step | What it does | | --------- | ---------------------------------------------------------------------------------------------------------------- | | Trigger | Starts the journey. Every automation has one, and it cannot be deleted. | | Action | Does something to the contact. See below. | | Wait | Pauses for a **Duration** (1 to 365) in **Hours** or **Days**. | | Condition | Branches. Contacts follow **Yes** (left) or **No** (right). Choices: Has tag, Field equals value, Is subscribed. | | End | Contacts who reach it have completed the automation. A path with no next step also completes. | A branch with nothing connected ends the journey for that contact. It never falls through to the other branch. Each step shows a count of the contacts waiting on it, and a step with contacts on it cannot be deleted until they move on. ### Actions - **Send email:** pick a **Campaign**. Its subject and design go to the contact. The campaign is not sent to its own audience and can stay a draft. Sends count toward that campaign's stats and your monthly sending limit. - **Add tag** and **Remove tag:** enter a **Tag name**. - **Update field:** set First name, Last name or Status (Subscribed, Unsubscribed or Pending). - **Send notification:** a **Subject** and an optional **Message**, emailed to the workspace owner when a contact reaches the step. - **HTTP request (webhook):** a **URL**, a **Method** (POST or GET) and an optional JSON body that can use `{{firstName}}`, `{{lastName}}` and `{{email}}`. Requests to private or internal addresses are refused, redirects are not followed, and a request that fails is not retried. A Send email step skips the contact, who then moves on, if no campaign is chosen, the contact is not Subscribed or is suppressed, your guardrails block the send, the monthly limit is reached, no mailing address is set, or the workspace is suspended. During quiet hours the contact waits at the step and the email goes out when they end. See [Sending Health](/docs/concepts/sending-health) and [Compliance](/docs/concepts/compliance). ## Triggers Pick one under **Trigger event**. | Trigger | Starts when | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | New subscriber joins | A Subscribed contact is created with Add contact or the [Contacts API](/docs/api/contacts), or someone confirms a double opt-in signup. | | Tag added to contact | Tags are added by editing a contact or through the API. Set **Tag** to match one tag, or leave it blank for any tag. | | Link clicked in email | A person clicks a tracked link. Set **Link contains** to match part of the URL, or leave it blank for any link. | | Subscribed through an integration | A connected integration subscribes a contact. | | Custom event from your app | Your app posts an event to `POST /api/v1/events`. Set **Event name**; case and spacing are ignored. Page views from site tracking arrive as `page_viewed`. | CSV imports, bulk Add tag and tags added by another automation do not start triggers. See [Double Opt-In](/docs/concepts/double-opt-in) and [Email Tracking](/docs/concepts/tracking). ## How contacts run through a flow - Contacts are enrolled only while the automation is **Live**. Someone who matches the trigger while it is a draft or paused is not enrolled later. - The trigger needs a step connected to it, or nobody is enrolled. - A contact is enrolled once per automation and is never restarted mid-journey. Turn on **Let contacts re-enter** to start a contact again, the next time the trigger fires, after they have finished or failed. - The engine runs every minute. Steps that happen right away run back to back, up to 10 in one run, and a Wait schedules the next step. - **Exit when**, above the canvas, ends the journey early: an event is recorded, a tag is added, or a link is clicked, with the value you enter. It is checked before every step. A goal with no value never matches. - A contact is marked failed if a step errors or the step no longer exists. ## Status: Draft, Live, Paused | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------- | | Draft | Being built. Nobody is enrolled. Click **Publish** and confirm to go Live. | | Live | Enrolling contacts and running steps. Click **Pause** to stop. | | Paused | No new enrollments, and contacts already in the flow stop where they are. Click **Resume**. | Paused contacts stay in the flow. Their waits keep counting, so a contact whose wait ended while paused moves on as soon as you resume. The builder has no way back to Draft. To keep a copy, use **Duplicate** on the Automations page, which creates a Draft. --- # Forms A form is a signup box you put on a site you already have, or share as a hosted page. Every submission becomes a contact. If you want a whole page instead of a box, build a [landing page](/docs/platform/landing-pages). ## Create a form Go to `Pages & Forms > Forms` and click **New form**. The editor opens on a form called "Untitled form" that asks for email and first name. Rename it in the top bar, change the settings on the left, and click **Save**. Nothing is saved until you click it. | Section | What it controls | | --------------- | ------------------------------------------------------------------------------- | | Subscribe to | The list new signups join, or "No list (collect only)". | | Fields shown | Email is always on. First name and Last name are optional. | | Button | Button text, background colour, text colour, corner radius (0 to 24 px). | | Success message | The text a visitor sees after submitting. Default: "Thanks! You're subscribed." | | How it appears | Display mode, and for overlays when they show up. | A form cannot ask for custom contact fields. It collects email, first name and last name only. ## Display modes and triggers Under **How it appears** you pick one of four display modes: - **Inline**: drawn in the page where you paste the snippet. - **Popup**: centred over the page with a dimmed backdrop. It keeps keyboard focus inside while open. - **Slide-in**: a card in the bottom right corner. It does not block the page. - **Banner**: a full-width bar pinned to the bottom. For the three overlay modes, **When it appears** adds a trigger: | Trigger | Setting | | ------------------------- | ---------------------------------------------------------------------------------------- | | As soon as the page loads | None. | | After a delay | Seconds on the page, 0 to 600. | | After scrolling | Percent of the page scrolled, 0 to 100. | | On exit intent | The pointer leaves toward the browser bar. Touch devices fall back to a 20 second delay. | Overlays also have **days hidden after someone dismisses it** (0 to 365, default 30). The choice is remembered in the visitor's browser, and a successful signup counts too. At 0 the form comes back on every visit. A visitor can close an overlay with the close button or Escape. ## Put it on your site Under the preview, the editor shows three ways to publish the form: - **Hosted page**: a stand-alone page at `https://flomailr.com/forms/FORM_ID`. It shows your workspace name above the form. Share the link, no embedding needed. It is always inline. - **HTML embed**: a plain `
` that posts to the form's submit address. It works on any page and is always inline. - **JS widget**: a script that draws the form and submits without reloading. Use it for popup, slide-in and banner. For an inline form, paste the whole snippet where the form should appear. The snippets carry a copy of your fields, colours and display settings from the moment you copy them. After you change those, copy and paste the snippet again. The list is read on Flomailr's side, so changing **Subscribe to** applies right away. If you delete a form, sites still embedding it show an error. To post from your own code instead, see the [Forms API](/docs/api/forms). ## What happens on submit - The address is trimmed and lowercased, and must look like an email. - Each IP address can submit five times in ten minutes. After that the submission is refused. - The contact gets the tag `form:FORM_ID`. The **Signups** column on the Forms table counts contacts with that tag. - The submission counts as "Submits a form" for [lead scoring](/docs/platform/settings) rules. Which list a signup joins and whether it needs confirming follow the same rules as every public signup: | Situation | Result | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | New address, form has a list | Saved as Pending. A "Confirm your subscription to LIST" email goes out under your workspace name. Clicking the link subscribes them, adds them to the list and records consent. | | New address, "No list (collect only)" | Subscribed straight away. No confirmation email, because the link is tied to a list. | | Already subscribed | Stays subscribed and is added to the list. | | Opted out before | With a list, status is unchanged and a confirmation email goes out. Clicking its link resubscribes them. With no list, nothing changes. | | Hard bounce on record | Refused with "This email address cannot be re-subscribed." | | Suppressed (bounce, complaint or manual) | Nothing is written. The visitor still sees the success message, so the form cannot reveal who is suppressed. | An address that is still Pending does not get a second confirmation email within 24 hours, and no address gets more than two in 24 hours. There is no per-form double opt-in switch. See [Double Opt-In](/docs/concepts/double-opt-in). The success message appears in every case above except the refusal, even when the person still has to confirm. Write it to fit, for example "Check your inbox to confirm." ## Spam protection Forms have no CAPTCHA or honeypot field. Protection comes from the per-IP limit, the confirmation throttle and double opt-in. A form never overrides a bounce, a complaint or a manual suppression. See [Compliance](/docs/concepts/compliance) and [Contacts](/docs/platform/contacts). --- # Landing Pages A landing page is a full web page that Flomailr hosts for you at its own address. You build it in the same block editor you use for emails, and a Signup block turns visitors into subscribers. For a signup box on a site you already have, use a [form](/docs/platform/forms) instead. ## Build a page Go to `Pages & Forms > Landing pages` and click **New page**. This creates a draft called "Untitled page" and opens the builder. - The block list is on the left, the canvas is in the middle, and the right panel edits whatever you select. - Rename the page in the top bar. The title is saved when you click away. - Use the undo and redo buttons, and the **Desktop** and **Mobile** preview toggle, to check your work. - Changes save on their own about two seconds after you stop. The top bar shows Saving, Saved or Save failed. - In the landing pages table, the duplicate button copies a page, including its SEO fields, as "TITLE (Copy)" with a new address. The copy starts as a draft. When no block is selected, the right panel shows **Page settings**. It has three parts: | Part | Fields | | ----------- | --------------------------------------------------------------------------------------------------------- | | Page URL | Slug, **Copy URL** and **Save**. | | Page styles | Page background, Content background, and Font family (Inter, Georgia, Fira Code, Helvetica or System UI). | | SEO | SEO title (up to 60 characters) and Meta description (up to 160). | ## Address and slug A published page lives at `https://flomailr.com/p/YOUR-SLUG`. There is no workspace name in the address, so a slug is unique across every workspace, not only yours. - A new page gets a slug from its title: lower case, every run of characters other than a to z and 0 to 9 becomes one hyphen, and leading and trailing hyphens are dropped. It is cut to 60 characters. Letters with accents count as separators. An empty result becomes `page`. - If the slug is taken, Flomailr adds `-1`, `-2` and so on until it is free. - To change it, edit **Slug** under Page settings and press Enter or click **Save**. The same cleanup applies, and the address shown underneath is the one that was saved, which can differ from what you typed. - The old address stops working when you change the slug. ## Publish A new page is a draft, and a draft returns a 404 at its address. Click **Publish** in the top bar to make it live. The button then reads **Published**, and clicking it again takes the page down. Once it is live, the top bar shows the address with a copy button, and the landing pages table shows a link icon next to it. Published pages can be indexed by search engines. The SEO title falls back to the page title when it is empty, and the page carries a canonical link plus Open Graph and Twitter tags. To remove a page, click the delete button in the top bar. Its address then returns a 404, and this cannot be undone. ## Capture subscribers Add a **Signup** block (in the **Conversion** group) and select it. In the right panel: - **Email list**, **Send subscribers to**: the list new contacts join. A Signup block with no list selected cannot accept signups. - **Content**: Headline, Subtext, Email placeholder, Button text, and **Show name field** with a Name placeholder. The name field collects a first name. - **Button** and **Spacing**: colours, corner radius and padding. Keep the Signup block at the top level of the page. The live form is only drawn for top-level blocks, so one placed inside Columns or Section shows nothing. In an email, a Signup block also renders nothing. Each signup follows the same rules as a [form](/docs/platform/forms) submission: - Each IP address can sign up five times in ten minutes. - With a list, a new address is saved as Pending and gets a confirmation email. It becomes Subscribed when they click the link. The visitor sees "Almost there! Check your inbox to confirm your subscription." - Someone already subscribed sees "You're already subscribed, you're all set!" - An address that has bounced is refused with a message asking for a different email. Other suppressed addresses see the ordinary message and are left unchanged. - Consent is recorded with the source `landing_page`. See [Double Opt-In](/docs/concepts/double-opt-in) for the full rules. Landing page signups are not tagged, and the table has no signup count. To see who joined, open the list under `Audience > Lists` (see [Lists](/docs/platform/lists) and [Contacts](/docs/platform/contacts)). --- # Analytics Flomailr reports on sending in two places: the Overview for your whole workspace, and a separate page for each sent campaign. Both read the same open, click, bounce and unsubscribe records. ## The Overview Open `Overview` in the sidebar (`/dashboard`). The period switch at the top right sets the window: **7d**, **30d** or **90d**. It lives in the address as `?period=7`, `?period=30` or `?period=90`. Anything else falls back to 30 days. The old `/dashboard/analytics` address redirects here and keeps the period. The top strip shows three totals: - **Subscribers**: contacts with status Subscribed, all time. The hint shows how many more are awaiting confirmation. - **Campaigns sent**: campaigns with status Sent, all time. - **Emails delivered**: emails sent by campaigns whose send date falls inside the period. Four rate cards cover campaigns sent inside the period. Each divides by emails sent in that window: | Card | What it counts | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Open rate | Opens recorded, one per person per campaign. This includes automated opens (see below). | | Click rate | People who clicked at least one link, counted once per campaign. Total clicks are not used, so a link scanner or a double click cannot push it up. | | Bounce rate | Messages that bounced. | | Unsubscribe rate | The unsubscribes counted against those campaigns. See the note below. | Each card also shows the change against the previous window of the same length, in percentage points, and a small daily trend line. A dash means nothing was sent in the period. For bounces and unsubscribes, down is the good direction. Three charts follow: - **Opens Over Time**: opens and clicks per day. These are counted on the day the event happened, not the day the campaign went out, so the totals can differ from the rate cards. - **Campaign Performance**: open rate and click rate for your six most recent sent campaigns. - **Engagement Heatmap**: opens by day of the week and hour, in local time. Days and hours use your workspace time zone, set at `Settings > Guardrails` under **Default time zone**. It falls back to UTC if the zone is not recognised. Below the charts, **Recent campaigns** lists your five newest. Sent campaigns link to their analytics, and the others link to the editor. Until an AI app or an API key is connected, the Overview also shows a **Connect your AI** prompt. ## Campaign analytics Go to `Campaigns > All campaigns` and click **Analytics** on a sent campaign. The page is at `/dashboard/campaigns/ID/analytics`. The header shows when it was sent and to which list or segment, and **Export CSV** downloads the data. | Card | Meaning | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sent | Recipients delivered. | | Human open rate | Opens with evidence of a person, divided by sent. The hint gives the rate including machine and Apple MPP opens. | | Click rate | Unique clickers divided by sent. The hint also shows total clicks. | | Bounce rate | Bounced divided by sent. | | Unsubscribed | Unsubscribes divided by sent. See the note below. | | Revenue | Appears only when there is revenue. Purchase events that carry a value are credited to the last campaign the buyer clicked or opened in the 7 days before buying. A click wins over an open at the same moment. | Further down the page: - **A/B test results**: for an A/B campaign, sent, human open rate and click rate per variant. Once it is sent you can **Declare winner**. - **Engagement Timeline**: opens and clicks over time. Pick a range (24h, 7d, 30d, All) and Hourly or Daily. - **Top links**: up to 20 most clicked links, with clicks and share of all clicks. - **Recent activity**: the latest opens and clicks, up to 30, with the address and how long ago. The CSV has three parts: a one-row summary, the link breakdown, and every open and click with email, link and timestamp. Its `open_rate` column uses all recorded opens, not only human ones. Note on unsubscribes: an unsubscribe link identifies the contact, not the campaign, and nothing currently adds to a campaign's unsubscribe count. These figures can read 0 even when people have opted out. To see opt-outs, filter `Audience > Contacts` by the Unsubscribed status, or open `Settings > Suppressions`. ## Open tracking and Apple Mail Privacy Protection An open is recorded when the tiny tracking image in an email is loaded. Not every load means a person read the message. Apple Mail Privacy Protection loads images for every message it delivers, and some security scanners and link previewers do the same. Flomailr sorts each open by the software that made the request: | Kind | Counts as human | | ------------------------------------------- | -------------------------------------------- | | Human | Yes | | Proxy cache (Gmail and Yahoo image proxies) | Yes, they load when the message is displayed | | Unknown (no software identified) | Yes | | Likely MPP (matches Apple's pre-fetch) | No | | Machine (bots, scanners, link previewers) | No | The match for Apple is a pattern, not proof. A background job re-checks Likely MPP opens: if one arrived more than 24 hours after the send, it is relabelled as human, because Apple fetches when a message is delivered, not when it is read. What this means for the numbers: - **Overview open rate** and the CSV count every recorded open, machine and MPP included. - **Human open rate** on the campaign page leaves out Likely MPP and Machine opens, so it reads lower. - Click rate is the number to trust most, since a click means someone followed a link addressed to them. - Contact engagement and the segment filters on it use human opens and clicks only. See [Contacts](/docs/platform/contacts). See [Tracking](/docs/concepts/tracking) for how links and the tracking image work, and [Campaigns](/docs/platform/campaigns) for sending. --- # Settings Settings holds everything about your workspace, how it sends and how other tools connect to it. Open `Settings` in the sidebar and switch between pages with the tab strip along the top. ## Roles Every person in a workspace has one role. | Role | What it can do | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Owner | One per workspace. Everything an admin can do, plus granting the Admin role, changing or removing an admin, and handing ownership on with **Make owner** (the previous owner becomes an admin). The owner cannot be removed or demoted. | | Admin | Manages the workspace: sender identity, custom domain, brand, API keys, webhooks, integrations, guardrails, lead scoring, paid tiers, suppressions and the team. Can invite and remove Members, but cannot create other admins. | | Member | Works on campaigns, contacts and automations. Can read most settings but cannot change workspace settings. Can leave the workspace. | Seven tabs are hidden from Members: API keys, Webhooks, Integrations, Guardrails, Lead scoring, Paid tiers and Activity. Every other tab shows for all roles, though on Workspace, Brand and Suppressions only owners and admins can save changes. ## Workspace The home tab, at `Settings`. It holds your **Workspace name**, member count and plan, a summary of your account, and three more cards: - **Email sending**: **From name** (up to 100 characters), an optional **Reply-to address**, and the **Business mailing address** (up to 300 characters). The mailing address goes in every email footer, and campaigns cannot be sent until it is set. The sending address is shown read-only. - **Custom contact fields**: extra fields for contacts, up to 30. Each has a label, a key and a type (Text, Number, Date or Yes/No). - **Custom sending domain**: send from your own domain. It needs the Pro plan. Enter the **Domain** and a **From address** prefix, add the DNS records Flomailr shows at your DNS provider, then click **Check verification**. Propagation can take up to 72 hours. Once it shows Verified, campaigns send from `prefix@domain`. New workspaces are created from **New workspace** in the sidebar's account menu. ## Brand Your brand kit, which your AI uses to style every email it builds. Click **Import from website** to prefill it from your homepage, then adjust. Nothing is saved until you click **Save brand kit**. Fields: business name, website, logo URL and width, colours (Primary, Accent, Text, Page background, Email background), base theme, heading and body fonts, a **Voice** note of up to 1000 characters, and social links. Everyone can view it. Only owners and admins can save. See [Design Emails with AI](/docs/ai/design-emails). ## Connect your AI The connector address to paste into Claude, Claude Code, Cursor or ChatGPT, with setup steps for each. ChatGPT is marked as not yet verified. It also has an API key route and a list of example prompts. Visible to everyone. Creating an API key needs an admin. See [Connect Your AI](/docs/ai/connect). ## Account Your own name and password, which follow you across every workspace. Changing the password signs you out everywhere else and disconnects your connected apps. Visible to everyone. ## Team Who is in the workspace and what they can do. Everyone sees the roster. Owners and admins can **Invite member** by email with a role, **Resend** or **Revoke** pending invitations, and **Remove** people. Only the owner can grant Admin or use **Make owner**. Invitations expire after 7 days. Seats count members plus pending invitations: Free allows 3, Pro 10, Enterprise has no limit. ## Topics Kinds of email a subscriber can decline without leaving your list, such as "Product updates". Each topic has a name (up to 60 characters) and an optional description (up to 200). Topics show on the preference page linked from every email footer. **Archive** hides a topic but keeps every opt-out, and **Restore** brings it back. Visible to everyone. ## Sending health Bounce rate and complaint rate against their limits (5% and 0.1%), your monthly sending quota against your plan, any suspension notice, and the ten most recent audit events. Automatic suspension applies once you have sent at least 500 emails and a rate goes over its limit. The quota is 2,000 emails a month on Free and 50,000 on Pro, and resets on the 1st. Visible to everyone. See [Sending Health](/docs/concepts/sending-health). ## Guardrails Rules the send path enforces on every message, including sends asked for by an AI app or an API key. Fields: **Enforce sending guardrails** (when off, sends are blocked rather than sent unchecked), **Daily send cap**, **Quiet hours** (sends inside the window are deferred, not dropped), **Default time zone**, **Check the suppression list**, **Agents must opt in to real sends**, and **Never send to these domains**. Click **Save guardrails**. Owners and admins only. See [Send Safely with AI](/docs/ai/send-safely). ## Suppressions Addresses this workspace will not send to, with a reason: Hard bounce, Spam complaint, Unsubscribed or Added by hand. Bounces and complaints land here automatically. You can search and filter by reason. Owners and admins can **Block address** and **Release** an entry. Members can read the list. ## Lead scoring Rules that give contacts points for what they do. Each rule has a name, an event (Opens an email, Clicks a link, Submits a form, Views a page or Custom event), an optional page path or event name, **Points**, and an optional **Limit per contact**. Points are whole numbers from -100 to 100, but not 0, and negative points lower a score. A contact's total stays between 0 and 1000. **Pause** a rule or delete it. Owners and admins only. ## Site tracking A script tag to paste before `` on every page of your own site, so page views can drive scoring rules and automations. Calling `flomailr.identify("customer@example.com")` attaches views to a known contact. It never creates a contact and never changes a subscription. Visible to everyone. ## Paid tiers Paid membership levels you can send to. Each tier has a **Tier name**, a **Stripe Price ID** (required, and unique per tier) and a display price. Membership is granted by the Stripe webhooks your integration receives. Flomailr never calls Stripe and cannot charge or refund anyone. Owners and admins only. ## Activity A read-only record of every action an AI app or API key took, such as Send, Dry run, Campaign saved or Suppressed, and what the send path decided. Filter by action. Owners and admins only. ## API keys Secret keys for the REST API. Click **Create API key**, give it a name, and copy the key straight away: it starts with `flo_sk_live_` and is shown once. The list shows each key's name, prefix, last use, creation date and status, and **Revoke** stops a key. Owners and admins only. See [API Keys](/docs/api/api-keys). ## Connected apps AI apps you have allowed to act in this workspace through the connector. **Disconnect** stops an app immediately, and it must ask for approval again to return. Owners and admins see every connection. Members see their own. ## Webhooks Send events to your own server as they happen. Add an endpoint URL with an optional label and choose events: Email opened, Link clicked, Email bounced, Spam complaint, Contact subscribed, Contact unsubscribed. A workspace can have 10 endpoints. The signing secret is shown once. A failed delivery is tried up to 6 times, and an endpoint is turned off after 20 failures in a row. You can **Pause** or **Enable** it, and the last deliveries are listed. Owners and admins only. See [API Overview](/docs/api/overview). ## Integrations Sync customers from Stripe, Shopify or WooCommerce into your audience. Pick a provider, add an optional label, a tag for new contacts and a list to add them to, then click **Connect** and paste the endpoint URL into the provider's webhook settings. Shopify's own marketing consent is used directly. Stripe and WooCommerce contacts arrive as Pending unless you tick **Subscribe these customers to marketing automatically**. Owners and admins only. --- # API Overview The Flomailr REST API lets your own code manage contacts, lists, campaigns, automations and forms, record events, and send transactional email. Every route lives under one base URL: ```text https://flomailr.com/api/v1 ``` ## Authentication Every request needs an API key in a bearer header: ```bash curl https://flomailr.com/api/v1/contacts \ -H "Authorization: Bearer flo_sk_live_..." ``` A key belongs to one workspace, and every route only sees that workspace's data. A contact, list, campaign, automation or form from another workspace answers `404`, the same as one that does not exist. Create and revoke keys under `Settings > API keys` (see [API Keys](/docs/api/api-keys)). A request with a missing, empty, unknown or revoked key gets `401` with code `UNAUTHORIZED`. ## Response format A single object comes back wrapped in `data`: ```json { "data": { "id": "clx...", "name": "Newsletter" } } ``` A list comes back with a `pagination` block: ```json { "data": [{ "id": "clx...", "name": "Newsletter" }], "pagination": { "total": 42, "limit": 20, "hasMore": true, "nextCursor": "clx..." } } ``` An error comes back as: ```json { "error": { "code": "VALIDATION_ERROR", "message": "name is required." } } ``` Successful `DELETE` requests return `204` with no body, except `DELETE /keys/:id`, which returns the revoked key's id and `revokedAt`. ## Pagination List routes take two query parameters: | Parameter | Meaning | | --------- | -------------------------------------------------------------------------------------- | | `limit` | Items per page. Default 20, maximum 100. A value that is not a number uses the default | | `after` | A cursor. Pass the previous response's `nextCursor` to get the next page | `nextCursor` is `null` when there are no more pages. Lists are returned newest first. `total` is the count of all items matching your filters, not just the current page. ```bash curl "https://flomailr.com/api/v1/contacts?limit=100&after=clx123" \ -H "Authorization: Bearer $FLOMAILR_KEY" ``` ## Error codes | HTTP | Code | When | | ---- | ------------------------ | -------------------------------------------------------------------------------------------------- | | 401 | `UNAUTHORIZED` | Missing, invalid or revoked API key | | 402 | `QUOTA_EXCEEDED` | A send would go past your monthly sending limit | | 403 | `FORBIDDEN` | Sending is suspended or turned off for the workspace | | 404 | `NOT_FOUND` | The resource does not exist in your workspace | | 409 | `CONFLICT` | A duplicate email on contact update, or an Idempotency-Key problem on `/send` | | 422 | `VALIDATION_ERROR` | A field is missing or invalid. Most routes use 422. `/events` and `/send` use 400 for invalid JSON | | 422 | `CAMPAIGN_ALREADY_SENT` | You tried to edit, schedule or send a campaign that has already been sent | | 422 | `AUTOMATION_WRONG_STATE` | The automation status change is not allowed from its current status | | 500 | `INTERNAL_ERROR` | An unexpected failure. `POST /send` returns 502 when every recipient fails to send | ## Resources - [API Keys](/docs/api/api-keys): create, list and revoke keys - [Contacts](/docs/api/contacts): create, update and look up subscribers - [Lists](/docs/api/lists): lists and list membership - [Campaigns](/docs/api/campaigns): create, schedule and send campaigns - [Automations](/docs/api/automations): list, edit, publish and pause flows - [Forms](/docs/api/forms): signup forms and their embed URL - [Events](/docs/api/events): record events from your app so automations can react - [Transactional Send](/docs/api/send): send one message to 1 to 5 people ## MCP and AI agents The same workspace is also reachable through the hosted MCP connector. See [Connect Your AI](/docs/ai/connect). --- # API Keys An API key is a long secret that identifies one workspace. You use it as a bearer token for the REST API, and you can also use it to connect an AI tool such as Claude Code to the hosted connector (see [Connect Your AI](/docs/ai/connect)). A key gives full REST access to its workspace, so treat it like a password. Keys look like this: ```text flo_sk_live_<32 hex characters> ``` ## Create and revoke keys in the dashboard Only workspace admins can see or manage keys. Go to `Settings > API keys`. 1. Create a key and give it a name (up to 100 characters). Use one key per integration so you can revoke them separately. 2. Copy the key when it appears. It is shown once and cannot be retrieved later. Flomailr stores only a hash. 3. To cut a key off, revoke it. A revoked key stops working immediately and stays in the list marked as revoked. The list shows each key's name, its first 12 characters (`flo_sk_live_`), when it was last used, and whether it is revoked. ## Manage keys over the API These routes use a key to manage the keys of the same workspace. Any valid key can call them. ### List keys ```text GET /api/v1/keys ``` Query: `limit`, `after` (see [pagination](/docs/api/overview)). Each item has `id`, `name`, `prefix`, `lastUsedAt`, `revokedAt` and `createdAt`. The key itself is never returned. ### Create a key ```text POST /api/v1/keys ``` ```json { "name": "CRM sync" } ``` `name` is required and at most 100 characters. The response is `201` and includes the key once: ```json { "data": { "id": "clx...", "name": "CRM sync", "prefix": "flo_sk_live_", "createdAt": "2026-10-04T12:00:00.000Z", "key": "flo_sk_live_..." } } ``` ### Get a key ```text GET /api/v1/keys/:id ``` Returns the same fields as the list. `404` if the id is not in your workspace. ### Revoke a key ```text DELETE /api/v1/keys/:id ``` Returns `200` with `{ "data": { "id": "...", "revokedAt": "..." } }`. Revoking an already revoked key is not an error. A key can revoke itself, which ends its own access. ## Keys and the connector When you connect an AI tool with an API key instead of signing in, the connector acts as that workspace and can use every tool except the workspace-switching ones. Actions the AI takes with a key are written to the activity log under `Settings > Activity`. See [Sending Safely with AI](/docs/ai/send-safely). --- # Contacts API Contacts are the people you email. All routes need a bearer API key (see [API Overview](/docs/api/overview)). ## The contact object ```json { "id": "clx...", "email": "ada@example.com", "firstName": "Ada", "lastName": "Lovelace", "tags": ["customer", "vip"], "status": "SUBSCRIBED", "createdAt": "2026-10-04T12:00:00.000Z" } ``` `status` is one of `PENDING`, `SUBSCRIBED`, `UNSUBSCRIBED` or `BOUNCED`. Only `SUBSCRIBED` contacts receive campaigns. See [Double Opt-In](/docs/concepts/double-opt-in). Limits: a contact can have at most 50 tags, and each tag at most 100 characters. ## List contacts ```text GET /api/v1/contacts ``` | Query | Meaning | | -------- | ----------------------------------------------------------------------------------- | | `limit` | Page size, default 20, maximum 100 | | `after` | Cursor from the previous page | | `status` | One of `PENDING`, `SUBSCRIBED`, `UNSUBSCRIBED`, `BOUNCED`. Anything else is a `422` | | `tag` | Only contacts that have this exact tag | | `q` | Case-insensitive search in email, first name and last name | Results are newest first. ## Create or update a contact ```text POST /api/v1/contacts ``` ```json { "email": "ada@example.com", "firstName": "Ada", "lastName": "Lovelace", "tags": ["customer"], "status": "SUBSCRIBED" } ``` - `email` is required. It is trimmed and lowercased. - `status` defaults to `SUBSCRIBED`. An unrecognized value also falls back to `SUBSCRIBED`. - This is an upsert on the email address. A new contact returns `201`. An existing contact is updated and returns `200`. - On an existing contact, the `tags` you send replace its tags (an omitted `tags` becomes an empty list). Always send the fields you want to keep. - A request can always lower a contact's status (to `UNSUBSCRIBED` or `BOUNCED`), but it never raises someone who unsubscribed back to `SUBSCRIBED` on its own say-so. That takes `"consent": "explicit"`, which records that the person themselves agreed (it is logged with the source `api`). `"consent": "declined"` records a refusal. Any other `consent` value is a `422`. A `BOUNCED` contact is never raised, and a `SUBSCRIBED` contact is never moved back to `PENDING`. The same rule applies to re-adding an address that was deleted after opting out. - Setting a contact to `UNSUBSCRIBED` or `BOUNCED` adds the address to the suppression list and removes the contact from every list. Setting it back to `SUBSCRIBED` later does not lift the suppression, so a suppressed address is still never mailed. - A contact saved as `SUBSCRIBED` starts any live automation whose trigger is a new subscriber. ## Get a contact ```text GET /api/v1/contacts/:id ``` Returns the contact object, or `404`. ## Update a contact ```text PATCH /api/v1/contacts/:id ``` Send only the fields to change: `email`, `firstName`, `lastName`, `tags`, `status`. - A new `email` that belongs to another contact in your workspace returns `409` with code `CONFLICT`. - An invalid email, an invalid `status`, a `tags` value that is not an array, or too many or too long tags returns `422`. - An empty `firstName` or `lastName` clears the field. - `tags` replaces the whole tag list. Adding a tag that the contact did not have starts any live automation triggered by that tag. - `UNSUBSCRIBED` and `BOUNCED` behave as in the create route above. ## Delete a contact ```text DELETE /api/v1/contacts/:id ``` Returns `204`. The contact is removed permanently. ## Lists a contact is on ```text GET /api/v1/contacts/:id/lists ``` Returns an array in `data` (it is not paginated). Each item has the list's `id`, `name` and `createdAt`, plus `addedAt`, the time the contact joined it. To add or remove a contact from a list, use the [Lists API](/docs/api/lists). ## Example ```bash curl -X POST https://flomailr.com/api/v1/contacts \ -H "Authorization: Bearer $FLOMAILR_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"ada@example.com","firstName":"Ada","tags":["customer"]}' ``` --- # Lists API A list is a named group of contacts that a campaign can be sent to. All routes need a bearer API key (see [API Overview](/docs/api/overview)). ## The list object ```json { "id": "clx...", "orgId": "clx...", "name": "Newsletter", "createdAt": "2026-10-04T12:00:00.000Z", "contactCount": 128 } ``` `contactCount` is the number of contacts on the list, whatever their status. ## List your lists ```text GET /api/v1/lists ``` | Query | Meaning | | ------- | ---------------------------------------- | | `limit` | Page size, default 20, maximum 100 | | `after` | Cursor from the previous page | | `q` | Case-insensitive search in the list name | ## Create a list ```text POST /api/v1/lists ``` ```json { "name": "Newsletter" } ``` `name` is required. Returns `201` with `id`, `name`, `createdAt` and `contactCount: 0`. ## Get, rename and delete a list ```text GET /api/v1/lists/:id PATCH /api/v1/lists/:id DELETE /api/v1/lists/:id ``` `PATCH` takes `{ "name": "New name" }` and requires a non-empty name. `DELETE` returns `204` and removes the list. It does not delete the contacts on it. Any of these returns `404` if the list is not in your workspace. ## List the contacts on a list ```text GET /api/v1/lists/:id/contacts ``` | Query | Meaning | | -------- | ---------------------------------------------------------- | | `limit` | Page size, default 20, maximum 100 | | `after` | Cursor from the previous page | | `status` | One of `PENDING`, `SUBSCRIBED`, `UNSUBSCRIBED`, `BOUNCED` | | `q` | Case-insensitive search in email, first name and last name | Items have the same shape as in the [Contacts API](/docs/api/contacts). ## Add contacts to a list ```text POST /api/v1/lists/:id/contacts ``` Send one contact or several: ```json { "contactId": "clx..." } ``` ```json { "contactIds": ["clx...", "clx..."] } ``` - Every id must be a contact in your workspace. If any id is unknown the whole request returns `404` and lists the missing ids. - Contacts already on the list are ignored. - Contacts whose status is `UNSUBSCRIBED` or `BOUNCED` are never added. They are counted as skipped instead of causing an error. ```json { "data": { "added": 2, "skipped": 1 } } ``` `added` counts contacts that were eligible, including any that were already on the list. ## Remove a contact from a list ```text DELETE /api/v1/lists/:id/contacts/:contactId ``` Returns `204`. The contact itself is not deleted. `404` if the list or the contact is not in your workspace. --- # Campaigns API Create email campaigns, point them at a list, schedule them or send them now, and read their results. All routes need a bearer API key (see [API Overview](/docs/api/overview)). ## The campaign object ```json { "id": "clx...", "name": "October newsletter", "subject": "What's new this month", "status": "SENT", "listId": "clx...", "scheduledAt": null, "sentAt": "2026-10-04T14:00:00.000Z", "createdAt": "2026-10-03T09:00:00.000Z", "stat": { "sent": 1200, "opened": 540, "clicked": 130, "bounced": 4, "unsubscribed": 2 } } ``` `stat` is `null` until the campaign has stats. `stat.unsubscribed` is not currently incremented by the unsubscribe flow, so it can read 0 even when people have opted out. Read opt-outs from contacts (status `UNSUBSCRIBED`) instead. A campaign created through the API stores the HTML you send in `body`. It is sent as written, with the unsubscribe footer and tracking added at send time. To design with blocks, use the dashboard builder or the [AI connector](/docs/ai/design-emails). Limits: `name` up to 200 characters, `subject` up to 998 characters, `body` up to 512 KB. ## List campaigns ```text GET /api/v1/campaigns ``` | Query | Meaning | | -------- | -------------------------------------------------------- | | `limit` | Page size, default 20, maximum 100 | | `after` | Cursor from the previous page | | `status` | `DRAFT`, `SCHEDULED` or `SENT`. Anything else is a `422` | | `q` | Case-insensitive search in name and subject | ## Create a campaign ```text POST /api/v1/campaigns ``` ```json { "name": "October newsletter", "subject": "What's new this month", "body": "...", "listId": "clx..." } ``` `name`, `subject` and `body` are required. `listId` is optional and must be a list in your workspace (`404` otherwise). The campaign is created as a `DRAFT` and nothing is sent. ## Get, update and delete ```text GET /api/v1/campaigns/:id PATCH /api/v1/campaigns/:id DELETE /api/v1/campaigns/:id ``` `PATCH` accepts `name`, `subject`, `body` and `listId` (use `null` to clear the list). Each field is validated like on create, and an empty value is rejected. A campaign that is already `SENT` returns `422` with code `CAMPAIGN_ALREADY_SENT`. `DELETE` returns `204`. ## Schedule a campaign ```text POST /api/v1/campaigns/:id/schedule ``` ```json { "scheduledAt": "2026-10-10T15:00:00Z" } ``` - The campaign needs a list first, otherwise `422`. - `scheduledAt` is an ISO 8601 date and time, and must be at least 30 seconds in the future. - A sent campaign cannot be scheduled (`CAMPAIGN_ALREADY_SENT`). - Returns `id`, `name`, `status` (`SCHEDULED`) and `scheduledAt`. To cancel, call `DELETE /api/v1/campaigns/:id/schedule`. It only works on a `SCHEDULED` campaign and sets it back to `DRAFT`. ## Send a campaign now ```text POST /api/v1/campaigns/:id/send ``` The body is optional JSON. Two optional fields exist: `excludeEmails` (an array of addresses to skip) and `overrideQuietHours` (`true` to ignore your quiet-hours window). The send goes through the same guardrails as a dashboard send: suppressed addresses and blocked domains are skipped, and your monthly plan limit is enforced. A send that would pass your daily cap, or that starts inside quiet hours, is refused as a whole with a `422`. A scheduled campaign that hits quiet hours is retried after they end. Your workspace also needs a physical mailing address in settings, because it is printed in every footer. See [Compliance](/docs/concepts/compliance) and [Sending Health](/docs/concepts/sending-health). Recipients go out in batches of 100. The request stops starting new batches after about 45 seconds. If recipients are still owed, the rest goes out in the background, picked up by a job that runs every minute: ```json { "data": { "sent": 800, "failed": 0, "remaining": 400, "complete": false } } ``` `sent` and `failed` count this call only. When `complete` is `false`, do not call again to finish the job: the rest goes out on its own, and a repeated call never mails anyone twice. Poll `GET /api/v1/campaigns/:id` until `status` is `SENT`. Errors: | HTTP | Code | When | | ---- | ----------------------- | ------------------------------------------------------------ | | 404 | `NOT_FOUND` | The campaign is not in your workspace | | 422 | `CAMPAIGN_ALREADY_SENT` | The campaign was already sent | | 403 | `FORBIDDEN` | The workspace is suspended | | 402 | `QUOTA_EXCEEDED` | The send would pass your monthly limit | | 422 | `VALIDATION_ERROR` | No audience, no subscribed contacts, or another send blocker | --- # Automations API Create automations, change their name or flow, publish or pause them, and read who is enrolled. All routes need a bearer API key (see [API Overview](/docs/api/overview)). The flow itself is easiest to build in the visual builder (see [Automations](/docs/platform/automations)). The API is for listing, publishing, pausing and for programmatic edits. ## The automation object ```json { "id": "clx...", "name": "Welcome series", "status": "LIVE", "createdAt": "2026-10-01T09:00:00.000Z", "updatedAt": "2026-10-02T09:00:00.000Z", "activeEnrollments": 14 } ``` `status` is `DRAFT`, `LIVE` or `PAUSED`. `activeEnrollments` counts enrollments that are still running. The record also carries a few other stored fields, but the flow is never included in list or detail responses. Fetch it from the builder. ## List automations ```text GET /api/v1/automations ``` Query: `limit`, `after`, and `status` (`DRAFT`, `LIVE` or `PAUSED`; anything else is a `422`). ## Create an automation ```text POST /api/v1/automations ``` ```json { "name": "Welcome series" } ``` `name` is optional and defaults to "Untitled automation". The new automation is a `DRAFT` with one trigger node, "New subscriber joins". Returns `201`. ## Get, update and delete ```text GET /api/v1/automations/:id PATCH /api/v1/automations/:id DELETE /api/v1/automations/:id ``` `PATCH` accepts any of: | Field | Notes | | ---------- | ------------------------------------------------------------ | | `name` | An empty name becomes "Untitled automation" | | `flowJson` | The flow, as `{ "nodes": [...], "edges": [...] }`. See below | | `status` | `LIVE`, `PAUSED` or `DRAFT`. See the transitions below | `flowJson` is checked before it is saved. Every node needs a string `id`, a string `type` and an object `data`. Every edge needs a string `source` and `target`. The engine uses node types `trigger`, `wait`, `condition`, `action` and `end`. Anything that does not match this shape returns `422`. Status changes: | To | Allowed from | Otherwise | | -------- | ------------------- | ---------------------------------------------- | | `LIVE` | `DRAFT` or `PAUSED` | `422` with code `AUTOMATION_WRONG_STATE` | | `PAUSED` | `LIVE` | `422` with code `AUTOMATION_WRONG_STATE` | | `DRAFT` | `DRAFT` only | A published automation cannot go back to draft | `DELETE` returns `204`. ## List enrollments ```text GET /api/v1/automations/:id/enrollments ``` Query: `limit`, `after`, and `status` (`ACTIVE`, `COMPLETED` or `FAILED`). ```json { "data": [ { "id": "clx...", "contactId": "clx...", "currentNodeId": "node_3", "status": "ACTIVE", "nextRunAt": "2026-10-05T09:00:00.000Z", "enrolledAt": "2026-10-04T09:00:00.000Z", "completedAt": null, "errorMessage": null } ], "pagination": { "total": 1, "limit": 20, "hasMore": false, "nextCursor": null } } ``` Results are newest first by `enrolledAt`. `errorMessage` is set when an enrollment failed. ## Starting automations from your app Contacts enter an automation through its trigger. To start one from your own system, record an event with the [Events API](/docs/api/events) and use a custom event trigger. --- # Forms API Create and manage signup forms. Each form has a submit URL you can post to from your own site. All routes need a bearer API key (see [API Overview](/docs/api/overview)). For the dashboard builder, embed code and display options, see [Forms](/docs/platform/forms). ## The form object ```json { "id": "clx...", "name": "Footer signup", "listId": "clx...", "fields": ["firstName"], "style": {}, "successMessage": "Thanks! You're subscribed.", "createdAt": "2026-10-01T09:00:00.000Z", "updatedAt": "2026-10-01T09:00:00.000Z", "embedUrl": "https://flomailr.com/api/forms/clx.../submit" } ``` - `fields` lists the optional fields shown next to email, for example `["firstName", "lastName"]`. - `style` holds the form's look (button text and colours, background, corner radius). It is `{}` until the builder or a `PATCH` sets it. - `listId` is the list that new subscribers join. It can be `null`. - `embedUrl` is the URL the form submits to. ## List forms ```text GET /api/v1/forms ``` Query: `limit`, `after` (see [pagination](/docs/api/overview)). ## Create a form ```text POST /api/v1/forms ``` ```json { "name": "Footer signup", "listId": "clx...", "fields": ["firstName", "lastName"], "successMessage": "You're in." } ``` - `name` is required. - `listId` is optional. If given it must be a list in your workspace (`404` otherwise). - `fields` defaults to `["firstName"]`. Non-string entries are dropped. - `successMessage` defaults to "Thanks! You're subscribed." Returns `201`. ## Get, update and delete ```text GET /api/v1/forms/:id PATCH /api/v1/forms/:id DELETE /api/v1/forms/:id ``` `PATCH` accepts `name` (cannot be empty), `listId` (a list id, or `null` to detach), `fields` (an array of strings), `successMessage` and `style` (an object). `DELETE` returns `204`. The API does not expose a form's display mode or trigger settings. Set those in the dashboard. ## Submitting a form The `embedUrl` is a public endpoint. It needs no API key, so a browser on your own site can post to it directly. It allows cross-origin requests. ```text POST /api/forms/:id/submit ``` Send JSON or a standard form post with `email` (required) and optionally `firstName` and `lastName`: ```bash curl -X POST https://flomailr.com/api/forms/FORM_ID/submit \ -H "Content-Type: application/json" \ -d '{"email":"ada@example.com","firstName":"Ada"}' ``` A successful post returns the form's success message: ```json { "ok": true, "message": "Thanks! You're subscribed." } ``` - An invalid or missing email returns `400` with `{ "error": "..." }`. - Each IP address can submit five times in ten minutes. After that the endpoint returns `429`. - An address that has bounced returns `400` ("This email address cannot be re-subscribed."). Other suppressed addresses get the normal success message and are left unchanged. - Whether the new contact is subscribed straight away or must confirm by email first depends on the signup rules. See [Double Opt-In](/docs/concepts/double-opt-in). --- # Events API Record something that happened in your own app, such as a purchase or a signup, so an automation can react to it. Events are attached to a contact. All routes need a bearer API key (see [API Overview](/docs/api/overview)). ## Record an event ```text POST /api/v1/events ``` ```json { "name": "order_placed", "email": "ada@example.com", "properties": { "order_id": "1042", "items": 3 }, "value": 59.9, "currency": "USD", "externalId": "order-1042" } ``` | Field | Notes | | ------------ | -------------------------------------------------------------------------------------------------------------- | | `name` | Required. See name rules below | | `email` | The contact's email. Provide `email` or `contactId` | | `contactId` | The contact's id. Used instead of `email` when both are given | | `properties` | Optional object. At most 50 keys and 8 KB when serialized | | `value` | Optional money amount in major units (for example dollars). Stored as whole minor units, so 19.99 becomes 1999 | | `valueCents` | Optional money amount as a whole number of minor units. Wins over `value` if both are sent | | `currency` | Optional three-letter code, stored uppercase. Anything else is ignored | | `externalId` | Optional. Your own unique id for the event, used to make retries safe. Truncated to 200 characters | ### Event names Names are normalized to lowercase snake_case and cut to 64 characters, so `Order Placed`, `order-placed` and `orderPlaced` all become `order_placed`. An automation trigger name is normalized the same way, so it still matches. ### Contacts - With `contactId`, the contact must be in your workspace or you get `404`. - With `email`, an existing contact is used. If there is none, a new contact is created with status `PENDING`, never `SUBSCRIBED`. An event is not a marketing opt-in. See [Double Opt-In](/docs/concepts/double-opt-in). ### Retries If you send an `externalId` that already exists in your workspace, nothing new is recorded and no automation runs again. You get `200` with `{ "id": "...", "duplicate": true, "createdAt": "..." }`. ### Response A new event returns `201`: ```json { "data": { "id": "clx...", "name": "order_placed", "createdAt": "2026-10-04T12:00:00.000Z" } } ``` Recording the event also starts any live automation with a custom event trigger of that name for the contact, and passes the event to your lead scoring rules. If starting the automation fails, the event is still recorded and the request still succeeds. Errors: `400` if the body is not valid JSON, `422` for a missing name, a missing email and contactId, an invalid email, bad `properties`, or a bad money value. ## List events ```text GET /api/v1/events ``` | Query | Meaning | | ----------- | ----------------------------------------- | | `limit` | Page size, default 20, maximum 100 | | `after` | Cursor from the previous page | | `name` | Only events with exactly this stored name | | `contactId` | Only events for this contact | Items have `id`, `name`, `properties`, `valueCents`, `currency`, `contactId` and `createdAt`, newest first. Because `name` is matched exactly, filter with the normalized form, for example `order_placed`. --- # Transactional Send Send one message that a person's own action asked for, such as a password reset, a receipt or a confirmation code. It goes to one to five recipients. For anything bulk, use a [campaign](/docs/api/campaigns). ```text POST /api/v1/send ``` Needs a bearer API key (see [API Overview](/docs/api/overview)). ```json { "to": "ada@example.com", "subject": "Your receipt", "html": "

Thanks for your order.

", "text": "Thanks for your order.", "replyTo": "support@example.com" } ``` | Field | Notes | | --------- | ------------------------------------------------------------------------------------------------------------- | | `to` | Required. One address or an array of up to 5. Duplicates are removed. Any invalid address rejects the request | | `subject` | Required. Control characters are stripped and the subject is cut at 500 characters | | `html` | The HTML body. Send `html`, `text`, or both | | `text` | The plain text body. If you send only `text`, it is wrapped in a minimal HTML page | | `replyTo` | Optional address. Defaults to the workspace reply-to, if one is set | The message comes from your workspace's sending address: your verified custom domain if you have one, otherwise the shared Flomailr address, with your workspace's from name. ## Response ```json { "data": { "sent": 1, "failed": 0, "blocked": [] } } ``` `blocked` lists recipients that were skipped and why, as `{ "email": "...", "reason": "..." }`. Reasons are `SUPPRESSED_HARD_BOUNCE`, `SUPPRESSED_COMPLAINT`, `SUPPRESSED_MANUAL` and `BLOCKED_DOMAIN`. If every recipient is blocked the request returns `422`. If every send attempt fails it returns `502`. ## How it differs from campaign mail Transactional mail follows different rules on purpose: - No unsubscribe footer, no `List-Unsubscribe` header, and no postal address requirement. - No open or click tracking is added. - A marketing unsubscribe does not block it. Someone who left your newsletter can still get a password reset. - Quiet hours and the daily send cap do not apply. These still apply: - Addresses suppressed for a hard bounce, a spam complaint or a manual block are skipped. - Domains in your blocked-domains list are skipped. - The workspace must not be suspended, and the guardrails master switch must be on (otherwise `403`). - The message counts toward your monthly sending limit. A send that would pass it returns `402` with code `QUOTA_EXCEEDED`. See [Compliance](/docs/concepts/compliance) for where the line between transactional and marketing mail sits. ## Idempotency Send an `Idempotency-Key` header to make retries safe: ```bash curl -X POST https://flomailr.com/api/v1/send \ -H "Authorization: Bearer $FLOMAILR_KEY" \ -H "Idempotency-Key: receipt-1042" \ -H "Content-Type: application/json" \ -d '{"to":"ada@example.com","subject":"Your receipt","text":"Thanks for your order."}' ``` - A repeat with the same key and the same request returns the original result without sending again, and the response includes `"replayed": true`. - The same key with a different request returns `409` with code `CONFLICT`. - The same key while the first request is still running returns `409`. ## Errors | HTTP | Code | When | | ---- | ------------------ | ----------------------------------------------------------------- | | 400 | `VALIDATION_ERROR` | The body is not valid JSON | | 422 | `VALIDATION_ERROR` | Missing or invalid recipients, subject or body, or all blocked | | 402 | `QUOTA_EXCEEDED` | The send would pass the monthly limit | | 403 | `FORBIDDEN` | The workspace is suspended or guardrails are off | | 409 | `CONFLICT` | Idempotency-Key reused with a different request, or still running | | 502 | `INTERNAL_ERROR` | Every recipient failed to send | --- # OpenAPI Spec Flomailr publishes its REST API as an OpenAPI 3.1 document. An AI coding agent such as Claude Code, Cursor or Codex can read it and write correct calls without guessing, and any tool that reads OpenAPI 3.1 can import it. ```text https://flomailr.com/openapi.json ``` The file is public and needs no API key. It describes every route under `/api/v1`: paths, query parameters, request bodies with their limits and allowed values, response shapes and error codes. It is the same API described on the other pages in this section. A test compares the spec to the route files, so a route cannot ship without being listed. ## Use it with a coding agent 1. Point the agent at the URL. For example: "Integrate with Flomailr using the API described at https://flomailr.com/openapi.json." 2. Give it an API key. A workspace admin creates one under `Settings > API keys` (see [API Keys](/docs/api/api-keys)). The key is shown once. Put it in an environment variable such as `FLOMAILR_API_KEY` and have the agent read it from there. Do not paste it into a prompt or commit it. 3. Ask for what you need: a typed client, a sync job, a signup handler. The agent sends the key as `Authorization: Bearer `. To look at the spec yourself: ```bash curl https://flomailr.com/openapi.json ``` The response is cached for an hour. ## What the agent should know first The top of the spec (`info.description`) covers these, and each operation repeats what matters for it. - **Consent.** `POST /contacts` creates a new address as `SUBSCRIBED` unless you send `status: "PENDING"`, which is you saying the person agreed to marketing email. An opted-out contact is not brought back by `status` alone; that needs `consent: "explicit"`. `POST /events` always creates unknown addresses as `PENDING`. See [Contacts](/docs/api/contacts). - **Sends reach real people.** `POST /campaigns/{id}/send` and `POST /send` email real people and cannot be undone. Have the agent create a draft, show it to you, and ask before it sends. - **Sending limits.** Campaign sends skip suppressed addresses and blocked domains, and are refused when they would pass the daily cap or the monthly plan limit or start inside quiet hours. Transactional sends skip bounced, complained and manually blocked addresses and count toward the monthly limit, but quiet hours and the daily cap do not apply. - **Retries.** Send an `Idempotency-Key` header on `POST /send` so a retry cannot send twice. Send an `externalId` on `POST /events` for the same reason. - **Pagination.** List routes take `limit` (up to 100) and `after`. Follow `pagination.nextCursor` until it is `null`. - **Errors.** Every error is `{ "error": { "code", "message" } }`. Read `message`: for example, a campaign send that is refused for the monthly limit can arrive as `422` rather than `402`. ## What is in the spec Contacts, lists, campaigns (create, edit, schedule, send), signup forms, automations and their enrollments, events, transactional send, and API keys. Fields the API does not return are not in the spec: a campaign's HTML body and an automation's flow can be written but never read back. ## What is not in the spec - The hosted MCP connector. If your AI app supports connectors, connect it instead of using REST. See [Connect Your AI](/docs/ai/connect). - The public form submit endpoint (`POST /api/forms/{id}/submit`). The spec only gives you each form's `embedUrl`. See [Forms](/docs/api/forms). - A request rate limit. The routes apply none of their own, so there is nothing to plan around besides the sending limits above. --- # Personalization Flomailr personalizes at send time, once per recipient. Merge tags insert a contact's details into the subject line and body. Conditional blocks show or hide a whole block depending on who is receiving the email. ## Merge tags There are four tags. Write them with double braces and no spaces inside. | Tag | Inserts | Empty when | | --------------- | ------------------------------------- | ----------------------------- | | `{{firstName}}` | First name | The contact has no first name | | `{{lastName}}` | Last name | The contact has no last name | | `{{fullName}}` | First and last name joined by a space | Both names are missing | | `{{email}}` | Email address | Never | - Tags are not case sensitive. `{{FirstName}}` works. - In the builder, use the **Personalize** menu next to **Subject line**, or the `{ }` button in the text toolbar. Both insert the tag for you. - Values are HTML-escaped in the body. In the subject, control characters are stripped. - The plain-text version of a campaign gets the same substitution. ## Empty and unknown tags A known tag with no value becomes an empty string. `Hi {{firstName}},` becomes `Hi ,` for a contact with no first name. There is no fallback syntax, so write copy that reads without the name, or use a conditional block (below). Anything else is sent exactly as typed. That covers a misspelled tag such as `{{firstname2}}`, a tag with spaces such as `{{ firstName }}`, and single braces such as `{firstName}`. The **Preflight** panel in the builder warns about unknown tags and single-brace tags before you send. ## Where tags are filled in | Message | Tags filled in | | ------------------------------------------- | -------------- | | Campaign sends, including scheduled and RSS | Yes | | Emails sent by an automation | Yes | | Test emails sent from the builder | No | | `send_email` and `POST /api/v1/send` | No | Test emails show tags as typed and include every conditional block. To see how a message resolves for a sample contact, use the `preview_personalization` tool (see [MCP tools](/docs/ai/tools)). For single sends, write the final text yourself. ## Conditional content Any block can carry a rule. Select the block and open **Show to** in the inspector, then turn on **Only some contacts**. Flomailr removes the block from the document before rendering, so each recipient gets an email that contains only their blocks. This works for blocks inside columns and sections too. A rule tests one field with one condition: | Field | Conditions | | ---------------------------- | ---------------------------------------------------------------- | | Email, First name, Last name | is exactly, is not, contains, does not contain, is set, is empty | - Comparison ignores case and surrounding spaces. - Only these three fields can be tested. Contact tags, list membership and custom fields cannot. - A rule with a condition that needs a value but has none is ignored, and everyone sees the block. An unknown or half-written rule also resolves to visible, because hiding content silently is harder to notice than showing too much. - A campaign with no rules renders once for the whole list. Only campaigns that use rules are rendered per recipient. Automation emails apply rules the same way. A common use: one heading shown when **First name** is set, and a second heading shown when **First name** is empty. ## Custom fields Custom contact fields (**Settings > Workspace > Custom contact fields**) are stored on the contact, can be imported and exported, and can drive segments. They are not merge tags, and a rule cannot test them. To send different content to different groups, build a segment and send to it. See also [Campaigns](/docs/platform/campaigns) and [Automations](/docs/platform/automations). --- # Email Tracking Flomailr tracks campaign emails in two ways: a tiny image records opens, and rewritten links record clicks. Both are signed per recipient, so a forged request cannot add to your numbers. Reports are on the campaign's analytics page and the Overview. See [Analytics](/docs/platform/analytics). ## Opens An open is recorded when the recipient's mail client loads the 1x1 image added to the end of the email. If the client blocks images, the open is not measured. Each person counts once per campaign: later opens by the same person are not added. Many image requests come from machines, not people, so Flomailr classifies each one: | Kind | Counts as a human open | Examples | | ------------------------------------ | ---------------------- | ----------------------------------------------------------- | | Human | Yes | A normal mail client or browser | | Image proxy | Yes | Gmail and Yahoo, which fetch the image when it is displayed | | Unknown | Yes | A request with no user agent | | Likely Apple Mail Privacy Protection | No | Apple's pre-fetch of every image in a delivered message | | Machine | No | Crawlers, scripts, security gateways, link previews | - **Human open rate** on a campaign counts the first three kinds. The hint under it shows the rate including machine and Apple opens. - Apple Mail Privacy Protection loads images whether or not the message is read. Flomailr spots it from the shape of the request. That is a heuristic, so a first fetch that arrives more than 24 hours after the send is reclassified as human by a background job. A pre-fetch happens at delivery, not a day later. - The **Engagement** card on a contact counts human opens and clicks only, so it reads lower than campaign totals. Contacts are Active (engaged in the last 30 days), Cooling (31 to 90 days), Dormant (longer ago), or Never engaged (they received mail and never opened or clicked). ## Clicks Every `http` or `https` link in the body is rewritten to a Flomailr address that records the click and redirects to your real link. The signature covers the destination, so the link cannot be edited to send someone elsewhere. - Not rewritten: `mailto:` and `tel:` links, anchors, relative links, the unsubscribe and preference links in the footer, and any link too long to encode. - Every click is recorded, repeat clicks included. **Click rate** uses unique clickers divided by sent. **Top links** lists clicks per URL. - Security gateways open every link in a message within seconds. A click is treated as a scanner when it comes from a machine user agent, or when the same recipient hits a different link of that message within 2 seconds. A scanner click is still recorded and redirected, but it does not start automations, score the contact, send a webhook or update engagement. ## Polls A poll block gives each answer its own link, and a vote is a click on that link. Results appear under **Top links**. Those are raw click counts, so a repeat click and a scanner click both add a vote. Treat small gaps with care. ## UTM parameters In the builder's document settings, **UTM Tracking** has the toggle **Append UTM parameters to links** and fields for Source, Medium, Campaign and Content. At send time Flomailr adds the fields you filled in to each link, before tracking is applied. Links that already carry a `utm_` parameter and the unsubscribe link are left alone. Campaign sends add them. Automation emails do not. ## Site tracking **Settings > Site tracking** gives you a script for your own site: ```html ``` It records page views as `page_viewed` events, which can feed lead scoring and automations. It never creates a contact and never changes a subscription status. A page view only moves scoring or automations when the visitor arrived from a tracked link in one of your emails. Calling `flomailr.identify("customer@example.com")` attributes page views to a contact for reporting, but an address anyone can type proves nothing, so it moves nothing. The identity lives in `sessionStorage` and ends with the browser session. ## What is not measured - Delivery per recipient. A message counts as sent when the mail provider accepts it. - The plain-text part of an email. It has no image and its links are not rewritten. - Which campaign an unsubscribe came from. The unsubscribe link identifies the contact only. - Test emails and previews. They carry no tracking. - Single sends. `send_email` and `POST /api/v1/send` add no image, rewrite no links and add no unsubscribe footer, so they record no opens or clicks, and their bounces are not counted in campaign stats. See [Transactional Send](/docs/api/send) and [Sending Safely with AI](/docs/ai/send-safely). Flomailr stores each open's user agent and a salted hash of the IP address. The raw IP address is never stored. --- # Compliance This page describes how Flomailr behaves around unsubscribes, postal addresses, suppression, consent and erasure. It is not legal advice. The law that applies to you depends on where you and your recipients are, and meeting it is your responsibility. ## Unsubscribe Every campaign and automation email carries a footer with **Update your preferences** and **Unsubscribe** links and your postal address. The message also carries the one-click `List-Unsubscribe` and `List-Unsubscribe-Post` headers that Gmail and Yahoo expect from bulk senders. - The footer link opens a page that asks "Unsubscribe?" and acts only when the button is pressed. Security scanners open every link in a message, and a link that unsubscribed on open would remove people who did nothing. The mail client's one-click header acts immediately. - An unsubscribe sets the contact to Unsubscribed, adds the address to the suppression list with the reason Unsubscribed, takes the contact off every list and sends a `CONTACT_UNSUBSCRIBED` webhook. - The confirmation page then offers the preference centre, as a second chance and not a condition. - Setting the contact back to Subscribed, importing the address or submitting a signup form does not lift the suppression. The person can lift it by confirming a subscription link or saving their preferences. An admin can release the address from the suppression list. ## Postal address Set **Business mailing address** in **Settings > Workspace**, under **Email sending**. It appears in the footer of every campaign email. Campaign sends are refused until it is set, whether you send from the dashboard, the API, a schedule or an AI agent. Automation emails are not sent until it is set either. ## Suppression list **Settings > Suppressions** lists addresses this workspace will not mail, with the reason: | Reason | How it gets there | | -------------- | --------------------------------------------------------------------------------- | | Hard bounce | A permanent bounce from the mail provider, or a contact marked Bounced | | Spam complaint | A recipient reports a message as spam, or a complaint status in an import | | Unsubscribed | The unsubscribe link or one-click, or a contact marked Unsubscribed | | Added by hand | **Block address** on this page (admins only), or a suppressed status in an import | Campaigns and automations never mail a suppressed address, even if the **Check the suppression list** guardrail is off. **Release address** (admins only) removes an entry. A person's own consent can clear an Unsubscribed entry: confirming a subscription link, or saving the preference centre. Nothing automatic clears a bounce, a complaint or a hand-added block. ## Consent log The **Consent log** card on a contact records when consent was given or withdrawn. Entries are written when: - a signup form, hosted subscribe page or landing page subscribes someone without a confirmation email, such as a form with no list - someone confirms a double opt-in email (source `double_opt_in`) - a contact re-subscribes through the preference centre - an integration reports an explicit opt-in or refusal - an SMS opt-out is processed - personal data is erased Imports, contacts added by hand and the contacts API do not write consent entries. The `check_compliance` tool (see [MCP tools](/docs/ai/tools)) checks email content and the share of a list with recorded consent. Pass `physicalAddress`, because the footer address is added at send time and is not part of the content it inspects. ## GDPR erasure On a contact, **Data & privacy** has **Erase personal data (GDPR)**. It cannot be undone. It: - replaces the email with `erased-ID@erased.invalid` and clears first name, last name, tags and custom fields - sets the status to Unsubscribed and removes the contact from every list - records an Erased entry in the consent log The contact row stays so campaign history keeps its counts. Engagement history stays but can no longer be traced to a person. Other fields on the record, such as phone number, time zone or Stripe customer ID, are not part of this action. Erasure does not add the old address to the suppression list, so a later import of it creates a new contact. ## Topics and preferences The **Update your preferences** link opens a hosted page. People choose how often they hear from you (**Every email**, **At most once a week**, **At most once a month**, or **Pause everything** for 30, 60 or 90 days or until they say otherwise) and which topics they want. Create topics in **Settings > Topics**, then set a campaign's **Topic** in the builder. Contacts who opted out of that topic are skipped. Weekly and monthly caps look at the last time the contact was sent anything (7 and 30 days). These checks run when a campaign is sent. Automation emails do not apply them, but still skip contacts who are not subscribed or are suppressed. ## Marketing and transactional | Rule | Campaigns and automations | `POST /api/v1/send` | | ------------------------------------------ | ------------------------- | ------------------- | | Unsubscribe footer and header | Added | Not added | | Postal address | Required | Not required | | Open and click tracking | Added | Not added | | Unsubscribed entries block the send | Yes | No | | Hard bounce, complaint, hand-added block | Block | Block | | Topics and frequency | Campaigns only | No | | Quiet hours and daily cap | Apply | Do not apply | | Monthly limit, suspension, blocked domains | Apply | Apply | `send_email`, the AI connector's tool for one to five recipients, follows the same rules with two differences: the daily cap applies to it, and every suppression entry blocks it, Unsubscribed included. Flomailr does not inspect a message to decide which kind it is. Sending promotional mail through the transactional route skips the unsubscribe link and the address, and that is your call to make. See [Transactional Send](/docs/api/send) and [Sending Health](/docs/concepts/sending-health). --- # Double Opt-In Double opt-in means a new subscriber has to click a link in a confirmation email before they can receive campaigns. It keeps mistyped and malicious addresses off your list, which protects your bounce and complaint rates. See [Sending Health](/docs/concepts/sending-health). ## It is always on for public signups There is no per-form, per-list or workspace setting. Every public signup works the same way: an embedded or hosted [form](/docs/platform/forms), the hosted subscribe page at `/subscribe/LIST_ID`, and the subscribe block on a [landing page](/docs/platform/landing-pages). The one exception is a signup with no list. A form set to **No list (collect only)** subscribes the person straight away and sends no email, because the confirmation link is tied to a list. ## Which entry points create which status | Entry point | Status | Confirmation email | | ------------------------------------------------- | ----------------------------------------------- | ------------------ | | Form, subscribe page or landing page, with a list | Pending | Yes | | Form with **No list (collect only)** | Subscribed | No | | `POST /api/v1/contacts` | Subscribed, unless you send a `status` | No | | `POST /api/v1/events` for an unknown email | Pending | No | | Stripe or WooCommerce integration | Pending, or Subscribed if you opt in (below) | No | | Shopify integration | Follows the customer's marketing consent | No | | CSV import | Subscribed, unless the file has a status column | No | | **Add contact** in the dashboard | Subscribed | No | - Only the public signup paths send a confirmation email. A contact created as Pending through an event or an integration stays Pending until the person signs up through a form and confirms, or you change its status. - An event is never an opt-in. A purchase or a custom event creates a Pending contact, not a Subscribed one. See [Events](/docs/api/events). - Stripe and WooCommerce do not record marketing consent. When you connect one in **Settings > Integrations**, new customers arrive as Pending unless you tick **Subscribe these customers to marketing automatically**. Shopify does record consent: opted-in customers are Subscribed and opted-out customers are Unsubscribed. - In a CSV, a `status` column of `pending` or `unconfirmed` imports a row as Pending. `subscribed`, `confirmed` and similar values import it as Subscribed. ## The confirmation email Flomailr sends "Confirm your subscription to LIST NAME" under your workspace's from name (or its name, if no from name is set). It comes from your verified custom domain if you have one, otherwise from the shared address. It says the person will not receive email until they confirm, and has one **Confirm subscription** button. Replies go to your reply-to address if you set one. The hosted subscribe page tells the person to check their inbox, and a form shows its own success message. Two limits stop a form from being used to flood someone's inbox: - An address that is still Pending does not get a second confirmation within 24 hours. - No address gets more than two confirmations in 24 hours. ## What confirming does The link is signed for one list and one contact. Clicking it: - sets the contact to Subscribed and adds them to the list - records a consent entry with the source `double_opt_in` - starts any automation with the **New subscriber joins** trigger If the link is invalid, the page says so and offers to subscribe again. Some signups behave differently: - **Already subscribed:** nothing changes. The contact is added to the list. - **Unsubscribed before:** the status is left alone and a confirmation email goes out. Clicking it resubscribes them and clears their old unsubscribe entry. A form cannot resubscribe someone on its own. - **Bounced:** refused. A bounced address is never resubscribed. - **Suppressed by a bounce, complaint or hand-added block:** nothing is written. The visitor still sees the usual success message, so the form does not reveal who is suppressed. Confirming never lifts these. ## Pending contacts do not receive mail Campaigns go to Subscribed contacts only, for lists and segments alike. Automation emails also skip anyone who is not Subscribed. A Pending contact can sit on a list, but nothing is sent to them. The contacts table shows Pending contacts by default, with their own status chip, because an unconfirmed signup is still a live prospect. Flomailr does not expire or delete them. See [Contacts](/docs/platform/contacts), [Lists](/docs/platform/lists) and [Compliance](/docs/concepts/compliance). --- # Sending Health Mailbox providers judge a sender by its bounces, spam complaints and authentication. Flomailr watches the first two, enforces a monthly quota, and checks your guardrails when it sends. **Settings > Sending health** reports what has happened. **Settings > Guardrails** sets what is allowed to happen. ## Bounces and complaints Flomailr receives bounce and complaint notices from its mail provider and matches each one to your workspace through the campaign that sent the message. Only permanent bounces and spam complaints count. A temporary bounce, such as a full mailbox, is ignored. | Event | Contact becomes | Suppression reason | | -------------- | --------------- | ------------------ | | Hard bounce | Bounced | Hard bounce | | Spam complaint | Unsubscribed | Spam complaint | The contact also comes off every list, the campaign's bounced or complained count goes up, and your `EMAIL_BOUNCED` or `EMAIL_COMPLAINED` webhook fires. Every later send skips the address. See [Compliance](/docs/concepts/compliance) for the suppression list. Single sends (`send_email` and `POST /api/v1/send`) carry no campaign, so they never count toward your rates. ## Thresholds and suspension **Email health rates** shows your bounce rate and complaint rate over every campaign you have sent. Each bar reads Healthy, Warning or Critical. | Rate | Warning from | Threshold | | ----------------------------- | ------------ | --------- | | Bounce rate (permanent) | 3% | 5% | | Complaint rate (spam reports) | 0.05% | 0.1% | If a rate rises above its threshold, Flomailr suspends the workspace automatically, but only once your campaigns and automations have sent at least 500 emails in total. Below that, one typo would swing the rate too far to mean anything. A suspended workspace cannot send campaigns, automation emails or single sends. The page shows an **Account suspended** banner with the reason, and the **Sending audit log** records it. Contact support to reinstate sending. ## Monthly quota Each plan has a quota of emails per calendar month. The count includes campaigns, automation emails and single sends. | Plan | Emails per month | | ---------- | -------------------------------- | | Free | 2,000 | | Pro | 50,000 | | Enterprise | 10,000,000, shown as "unlimited" | A campaign that would pass the quota is refused as a whole, with the number remaining, and is not sent in part. The page shows **Available**, **Nearly full** (80% used) or **Limit reached**, and a blocked send is logged as **Blocked (quota)**. ## Guardrails **Settings > Guardrails** holds rules the server enforces whatever started the send: the dashboard, an API key or an AI agent. `POST /api/v1/send` skips the daily cap and quiet hours, and `send_email` skips quiet hours. Only owners and admins can change the rules. | Setting | What it does | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Enforce sending guardrails** | On by default. If you turn it off, every send is blocked, not sent unchecked. | | **Limit sends per day** | Off by default. Sets **Messages per day**, counted per calendar day in your default time zone. A campaign that would pass it is refused whole. | | **Defer sends overnight** | Off by default. Sets a quiet window with **From** and **Until** hours. Uses each contact's time zone, else your **Default time zone** (UTC by default). | | **Check the suppression list** | On by default. Blocks anyone who hard-bounced, complained or opted out. | | **Agents must opt in to real sends** | Off by default. When on, agent sends are dry runs that report what would happen without delivering anything. | | **Never send to these domains** | One domain per line. Matching recipients are dropped. | If any recipient is inside quiet hours when a campaign send starts, the whole send is refused and nothing goes out. A scheduled campaign is retried until the window closes. An automation email waits until the window ends. ## Sender authentication Mail goes out from the shared Flomailr address unless you add a custom domain, which needs the Pro plan. In **Settings > Workspace**, open **Custom sending domain**, enter your root domain (such as `acme.com`, not `mail.acme.com`) and a **From address** prefix, and select **Add domain**. Publish the DNS records the page lists, then select **Check verification**. DNS can take up to 72 hours. Once the status is **Verified**, campaigns, automations and single sends use your address. Three DNS records decide whether receivers trust your mail: - **SPF** lists the services allowed to send as your domain. A record may cause at most 10 DNS lookups. - **DKIM** is a signature checked against a public key that your domain publishes. - **DMARC** tells receivers what to do with mail that fails the other two, and where to send reports. The free checker at `/tools/dmarc-checker` reads a domain's live SPF, DKIM, DMARC and MX records, counts SPF lookups against the limit of 10, and says what to fix. Add your DKIM selector (the part before `._domainkey`) to check one key. It needs no account and allows 20 checks per 10 minutes from one network. Before a large send, an AI agent can call `check_send_readiness`. It returns GO, CAUTION or NO_GO from your domain authentication, bounce and complaint rates, quota and suspension status. See [Sending Safely with AI](/docs/ai/send-safely) and [Settings](/docs/platform/settings).