# Marketing email your team and its agents can run Plainsend does what a marketing platform should: contacts with consent, templates, journeys and broadcasts. Every part is an API, a command-line tool and an MCP server, so your staff's agents can build it. A person approves what reaches customers. Your data is stored in Sydney. ### $1 — start now For small businesses and side projects. Bring a spreadsheet, a website or a CRM, verify your domain and send your first journey the same day. **$1 a month, billed yearly ($12).** 1,000 contacts and 5,000 emails a month, journeys included. Sign up and set up today; card checkout opens soon. [Start for $1](https://app.plainsend.app/auth/login?signup=1) · [See all plans](https://plainsend.app/pricing.md) ### Switching from Marketing Cloud or HubSpot For teams whose renewal is coming up. We move your contacts, consent history, templates and journeys with you, run beside your current tool, and time the switch to your renewal date. [Plan a switch](https://plainsend.app/switch.md) · [Compare costs](https://plainsend.app/calculator.md) ## What you get on every plan ### Journeys on every plan Branches, delays, goals, A/B splits and re-enrolment rules. Journeys are versioned documents: a change makes a new version, and people already in the journey finish on the old one. ### Consent you can prove Every opt-in and opt-out is recorded with its source, time and wording. Any opt-out on any record stops the person everywhere, and one-click unsubscribe is built in. ### Agents draft, people approve Your agent can build templates, segments and journeys, preview them and simulate them. Going live is an approval an owner gives, part by part, and a change after approval needs a new OK. ### Your data, where it lives Import spreadsheets, or connect HubSpot, Salesforce, Postgres or Databricks with a mapping file. Every sync can be run as a dry run first, and opt-outs are written back to the source. ### Contact safety Duplicate records are matched into one person, so nobody gets the same email twice. Frequency caps, preflight checks and a reputation guard stop sends that would hurt you. ### Marketing as code Keep brands, templates, segments and journeys as files in git. `plainsend workspace plan` shows what would change; `apply` saves drafts for approval. ## How it works 1. **Bring your contacts.** Upload a CSV or Excel file, send events from your app, or connect your CRM. Plainsend shows what an import would do before it does it. 2. **Verify your sending domain.** On Cloudflare, Plainsend can write the DNS records for you. Anywhere else it shows the records, checks them live, and gives you a link to send to whoever manages your DNS. 3. **Pick a starter journey.** Five ready-made packs: clinic starter, win-back, monthly newsletter, tax time, and software trial to paid. Tailor it yourself or ask your agent. 4. **Approve and send.** Preflight shows who will get it, who won't and why. You approve; Plainsend sends through Amazon SES, warming up your new domain over about three weeks. ## Built for agents as much as people Point Claude, or any MCP client, at `https://api.plainsend.app/mcp`. Your agent gets the same tools and the same rules as the console: drafts are free, sends need a person. [Set up your agent](https://plainsend.app/agents.md) · [Read the quickstart](https://plainsend.app/docs.md) **What Plainsend is not.** We don't do ads, social posts, a CMS or a sales CRM. We do email marketing (SMS is coming) and work with the CRM you already have. --- # Pricing You pay for contacts and emails, never for seats. Journeys and contact safety are on every plan. Prices are in US dollars. ### Solo **$1** a month, billed yearly ($12) - 1,000 contacts - 5,000 emails a month - Every feature, journeys included - Data from the API, webhooks and CSV or Excel files - "Sent with Plainsend" badge in the footer - A card on file keeps spammers out [Start for $1](https://app.plainsend.app/auth/login?signup=1) ### Starter **$29** a month - 5,000 contacts - 50,000 emails a month - No badge - Adds Google Sheets, Airtable and Zapier (coming) [Start with Starter](https://app.plainsend.app/auth/login?signup=1) ### Growth **$149** a month - 25,000 contacts - 250,000 emails a month - Native HubSpot and Salesforce connectors, with opt-outs written back - A/B splits in journeys; lead scoring and environments (coming) [Talk to us](https://plainsend.app/switch.md#talk) ### Scale **Usage**, $500 a month minimum - $1 per 1,000 contacts a month - $0.50 per 1,000 emails - SMS at carrier cost + 20% (SMS is coming) [Talk to us](https://plainsend.app/switch.md#talk) ### Enterprise **From $3,000** a month - Bring your own Amazon SES, or run Plainsend in your own AWS account (coming) - Single sign-on (coming) and an SLA - Data residency: stored in Sydney today - Warehouse audiences (Databricks today) and custom sources [Talk to us](https://plainsend.app/switch.md#talk) **Coming:** Card checkout for Solo and Starter is opening soon. You can sign up today: your workspace starts in the sandbox, so you can set everything up and send to your own team, and you choose a plan when checkout opens. Not sure which plan fits? The [cost calculator](https://plainsend.app/calculator.md) picks one from your contacts and emails, next to HubSpot's and Marketing Cloud's list prices. ## What counts - **Contacts** are the contacts you can email (not unsubscribed, bounced or suppressed), counted at the month's peak. - **Emails** are messages sent in a calendar month. - Getting close to a limit warns you first. On Solo, new sends pause at 120% of the monthly emails, with a plain reason and the plan that fits; on paid Starter and up, going over only warns. Contacts over the limit never stop a send. ## Questions ### Why $1, and why yearly? $1 a month covers what a small sender costs us to run. A card fee on a $1 monthly charge would cost more than the charge, so Solo is billed once a year. The card also keeps out people who want a free way to send spam. ### Are journeys really on every plan? Yes. Most tools keep branching automation for their expensive tiers. On Plainsend a Solo account gets the same journey engine as Enterprise. ### What is the "Sent with Plainsend" badge? A small line in the footer of Solo emails. It's how small senders help us grow. Starter and above remove it. ### Can I send right away? You can send a test to yourself straight away, and your new workspace can send to your own team. Sending to your contacts needs a paid plan, your own domain verified with SPF and DKIM, and your postal address in the footer (the law requires it). The Set up page in the console walks you through all three. A new domain warms up over about three weeks: a daily cap starting at 50 that rises as your mail is accepted, most engaged contacts first. ### Do you charge for seats or onboarding? No. Invite your whole team. There is no onboarding fee. Guided migration for teams switching from Marketing Cloud or HubSpot is [described here](https://plainsend.app/switch.md). ### Can I cancel? Yes, any time. Solo is billed yearly, so it runs until the end of the year you paid for. You can export your contacts and your whole workspace (`plainsend workspace export`) first. ### Where is my data stored? In Sydney, Australia. See [security](https://plainsend.app/security.md) for the details and the services we use. --- # Cost calculator Enter your contacts and monthly emails. The calculator picks the Plainsend plan that fits and puts it next to the public list prices of HubSpot Marketing Hub Professional and Salesforce Marketing Cloud Engagement. *The interactive calculator runs on https://plainsend.app/calculator. The prices and formulas it uses are listed below, so you (or your agent) can do the same sums.* **List prices are where negotiations start, not where they end.** Your invoice is the real number: put it in "What you pay today". Licence is also only part of the cost. Teams on Marketing Cloud usually pay for specialists (SQL, AMPscript) and agencies too, so add those if you know them. Running Plainsend takes time too; we don't pretend it is zero, and we don't estimate it here. ## Assumptions All prices in US dollars, per year, before tax. Each comes from a public source, read on the date shown. ### Plainsend | Plan | Price | Fits up to | |---|---|---| | Solo | $12 a year | 1,000 contacts, 5,000 emails a month | | Starter | $29 a month ($348 a year) | 5,000 contacts, 50,000 emails a month | | Growth | $149 a month ($1,788 a year) | 25,000 contacts, 250,000 emails a month | | Scale | $1 per 1,000 contacts a month + $0.50 per 1,000 emails, $500 a month minimum | No limit | | Enterprise | From $3,000 a month | Own SES or own AWS (coming), SSO (coming), SLA | The calculator picks the smallest plan whose contacts **and** emails both fit; above Growth it uses Scale. Source: our [pricing page](https://plainsend.app/pricing.md). ### HubSpot Marketing Hub Professional - About **$890 a month** with 3 seats and 2,000 marketing contacts, plus a one-time onboarding fee of about **$3,000**. Above 2,000 contacts HubSpot charges more in contact tiers; we don't have a public figure for every tier, so the calculator shows "more than" the 2,000-contact price. - HubSpot's list price has moved between $800 and $890 in 2026 (third-party price tracking, [pricingsaas.com](https://pricingsaas.com/companies/hubspot.marketing.starter/diffs/2026W31)). Check [hubspot.com/pricing](https://www.hubspot.com/pricing/marketing) for today's price. - HubSpot does not allow downgrades during a term ([HubSpot Customer Terms](https://legal.hubspot.com/terms-of-service), updated 2026-09-16). ### Salesforce Marketing Cloud Engagement List prices per org per month, billed yearly ([salesforce.com, Engagement pricing](https://www.salesforce.com/marketing/engagement/pricing/), read 2026-10-12): | Edition | Per month | Includes | |---|---|---| | Pro+ | $2,000 | 2.5M emails, 15,000 contacts, Marketing Cloud Growth | | Corporate+ | $5,500 | 10M emails, 1M mobile messages, 45,000 contacts, Marketing Cloud Advanced | | Enterprise+ | $30,000 | 150M emails, 10M mobile messages, 500,000 contacts, Marketing Cloud Advanced | - The calculator picks the smallest edition whose contacts fit. Check the email allowance against your contract. - Add-ons cost extra: Intelligence+ from $11,000 a month and Personalization+ from $15,000 per org a month ([Salesforce blog](https://www.salesforce.com/blog/next-gen-marketing-cloud-details/), 2025). - Subscriptions renew automatically for a year unless notice is given 30 days before the term ends, and a renewal at lower volume is re-priced without the old per-unit price ([Salesforce MSA §11.2](https://www.salesforce.com/en-us/wp-content/uploads/sites/4/documents/legal/salesforce_MSA.pdf), updated 2026-09-01). ## Want this on your real numbers? Send us your edition, contacts, sends and renewal date and we'll build a three-year comparison with you: stay, move to Marketing Cloud Next, or move to Plainsend. [Talk to us](https://plainsend.app/switch.md#talk). --- # Switching from Marketing Cloud or HubSpot Moving your marketing is a project, not a signup. We do it with you: your data and consent history first, one journey running beside your current tool, then the rest, all finished before your renewal notice is due. [Talk to us](#talk) · [Compare costs](https://plainsend.app/calculator.md) ## Start from your renewal date Most switches happen at renewal, so we plan backwards from it. - **Salesforce** subscriptions renew automatically for another year unless you give notice at least **30 days** before the term ends. A renewal with fewer contacts or sends is re-priced without your old per-unit price, so shrinking slowly can cost more than leaving cleanly ([Salesforce MSA §11.2](https://www.salesforce.com/en-us/wp-content/uploads/sites/4/documents/legal/salesforce_MSA.pdf), updated 2026-09-01). - **HubSpot** renews for the same term (up to a year) and doesn't allow downgrades during a term ([HubSpot Customer Terms](https://legal.hubspot.com/terms-of-service), updated 2026-09-16). | When | What happens | |---|---| | 9 months before renewal | First call: your edition, contacts, sends, brands, who signs, what security review needs | | 8 months | A cost model on your numbers, three years out: stay, move to Marketing Cloud Next, or move to Plainsend | | 7 months | Migration assessment: what maps one-to-one, what needs a rebuild, what we don't do. Security review starts | | 6 to 3 months | A paid pilot: one brand, two or three journeys, your real consent data, success criteria agreed up front | | Inside the pilot | A parallel run of at least three weeks (below) | | 45 days before | Decision, with the pilot results against the criteria | | 30 days before | You give your current vendor notice. You send it, never us | | Renewal day | Cutover: your old platform sends nothing | Leaving it later is possible; it just makes the pilot shorter. ## How the migration goes 1. **Suppression and consent first.** Unsubscribes, bounces and consent history move before any contact, so the first send can't reach someone who opted out. Plainsend's consent ledger keeps the source and time of each one. 2. **Contacts and data.** Plainsend connects to HubSpot and Salesforce directly (contacts, companies, deals, leads and opportunities), to Postgres and Databricks, or takes files. Every sync runs as a dry run first. Opt-outs are written back to HubSpot and Salesforce, so the two systems agree. 3. **Templates.** Paste or import your HTML emails: Plainsend converts them into editable templates in your brand kit, re-hosts the images and flags anything that won't render well. 4. **Journeys.** Each journey becomes a document in git that you can review line by line, simulate with a test contact, and approve. Your agent can do the first draft: HubSpot's own MCP server lets it read your workflows while it writes Plainsend journeys. 5. **Sending domain.** Keep your From domain and add Plainsend's DKIM records next to your current ones, or use a subdomain such as `mail.yourcompany.com`. A new sending setup warms up over about three weeks. **Coming:** **Migration tools for Marketing Cloud.** A read-only inventory of your Marketing Cloud account (journeys, data extensions, templates, AMPscript use) and a converter from Journey Builder to Plainsend journeys. Until then we map journeys with you by hand and with agents. ## The parallel run Nobody should switch everything in one night. During the pilot: - New signups and your suppression list go to Plainsend first. - One low-risk journey (a welcome series, say) moves to Plainsend for your most engaged contacts. Your current tool keeps everyone else. - We compare delivery, clicks, bounces and complaints side by side every week, and widen the move only when the numbers hold. - Mailbox providers expect a new sender to build up over about three weeks, so the parallel run lasts at least that long. Plainsend's warm-up caps handle the daily limits for you. ## Pay once, not twice Our offer to the first teams that switch: a fixed-price pilot, **credited against your first year**, with your Plainsend contract starting on your current renewal date. You don't pay for two platforms for a full year. ## Run it in your own AWS For regulated teams, the strongest answer to a security review is that the data never leaves your account. **Coming:** **Bring your own Amazon SES**: Plainsend sends through your SES account with a role you control, so the sending reputation and the bill are yours. **Plainsend in your AWS account (BYOC)**: the whole platform runs in your account, with your SES and your database. Both are being built with our first design partners. Ask us if you want to be one. Today, Plainsend stores data in Sydney and sends through Amazon SES in Sydney. See [security](https://plainsend.app/security.md). ## Talk to us Tell us what you use and when it renews. We store what you send here and reply ourselves; we don't add you to a mailing list. *The form is on https://plainsend.app/switch. An agent can send the same thing:* ```sh curl -X POST https://plainsend.app/lead -H 'content-type: application/json' \ -d '{"name":"Your name","email":"you@company.com","company":"Company","platform":"marketing-cloud","renewal":"2027-06","message":"What you want to talk about"}' ``` `platform` is one of `marketing-cloud`, `mc-next`, `hubspot`, `other`, `none`. A Turnstile check may be required. --- # Plainsend vs HubSpot Marketing Hub HubSpot is a good all-in-one: CRM, marketing, sales and service in one login. If you want all of that from one vendor, it's hard to beat. This page is for teams who want email journeys and consent without paying per contact tier and seat, or who want their agents to build marketing as code. Read on 2026-10-12. Check the sources for today's prices. ## Where HubSpot is strong - **One product for everything.** CRM, forms, landing pages, blog, ads, social, chat and reporting together. - **Easy to start.** A polished editor, a large template marketplace and lots of training (HubSpot Academy). - **An MCP server.** HubSpot's remote MCP server went GA on 2026-04-13, with write access to CRM records ([HubSpot changelog](https://developers.hubspot.com/changelog/remote-hubspot-mcp-server-is-now-generally-available)). ## Where Plainsend is different | | HubSpot Marketing Hub Professional | Plainsend | |---|---|---| | Price | About $890 a month with 3 seats and 2,000 contacts, plus about $3,000 onboarding | Starter $29, Growth $149 a month; no seats, no onboarding fee | | What grows the bill | Marketing contact tiers and seats | Contacts and emails | | Contract | Renews for the same term (up to a year); no downgrades during a term | Monthly on Starter and Growth; Solo yearly | | Journeys | Workflows on Professional and above | Journeys on every plan, including the $1 Solo plan | | Automated emails by API | Not available: workflow emails can't be created through HubSpot's API | Every template, journey and send is an API call, a CLI command and an MCP tool | | Approvals | Enterprise | Every plan: what an agent wants to put live waits for an owner's approval, part by part | | Duplicates | Merge (not reversible) | Records linked into one person automatically; one email per person per send | ## Keep HubSpot as your CRM You don't have to leave HubSpot to use Plainsend. The HubSpot connector reads contacts, companies and deals, takes consent per subscription type, and writes unsubscribes back to HubSpot, so the two systems agree. ## What Plainsend doesn't do - No CRM, sales pipeline, chat, blog, landing pages, ads or social publishing. - No hosted forms yet (coming). Today, forms post to your own site or app, which sends the contact to Plainsend's API or a signed webhook. - No drag-and-drop page builder; the email builder is MJML-based, and HTML import works. - No SMS yet (coming). - A small company. You'll be talking to the people who build it. ## Sources - HubSpot Marketing Hub Professional: about $890 a month with 3 seats and 2,000 contacts, plus about $3,000 onboarding. The list price moved between $800 and $890 during 2026 (third-party tracking, [pricingsaas.com](https://pricingsaas.com/companies/hubspot.marketing.starter/diffs/2026W31)); today's price is at [hubspot.com/pricing](https://www.hubspot.com/pricing/marketing). - Renewal and downgrade terms: [HubSpot Customer Terms of Service](https://legal.hubspot.com/terms-of-service) (updated 2026-09-16). - API limits on automated emails: found building a HubSpot integration in 2026 (we could not create AUTOMATED emails or set the email footer through the API). [Start for $1](https://app.plainsend.app/auth/login?signup=1) · [Plan a switch](https://plainsend.app/switch.md) · [Cost calculator](https://plainsend.app/calculator.md) --- # Plainsend vs Salesforce Marketing Cloud Marketing Cloud is a big, capable suite. If you use most of it, stay. This page is for teams who mainly need email journeys, consent and a clean link to Salesforce, and find themselves paying for, and staffing, much more. Read on 2026-10-12. Salesforce changes its editions often: check the sources. ## Where Marketing Cloud is strong - **Breadth.** Email, SMS, push, advertising audiences, personalisation and analytics from one vendor, with Data Cloud (now Data 360) underneath. - **Scale and track record.** Very large senders run on it, and there is a large partner and consultant market. - **Salesforce itself.** If your CRM is Salesforce, it's the incumbent's own product, with Marketing Cloud Connect and now Marketing Cloud Next built on the core platform. ## Where Plainsend is different | | Marketing Cloud Engagement / Next | Plainsend | |---|---|---| | Pricing model | Editions per org per month, billed yearly; channels draw on a credit pool; add-ons extra | Contacts plus emails; no seats, no modules ([pricing](https://plainsend.app/pricing.md)) | | Entry list price | Pro+ $2,000 a month, Corporate+ $5,500, Enterprise+ $30,000 | Growth $149 a month; Scale from $500 | | Building a journey | Journey Builder in the browser; day-to-day work often needs SQL and AMPscript | A journey is a document in git: review it, simulate it, approve it. Or build it in the console | | Changing a live journey | An active version can't be edited; you publish a new version | The same: a new version, people already in it finish on the old one. Plan shows the change first | | Agents | Agentforce Marketing, which expects Data Cloud | An MCP server and CLI for any agent (Claude, others). Writes are drafts; a person approves | | Consent | Configured per business unit | A consent ledger (source, time, wording) and "any opt-out stops the person everywhere" by default | | Salesforce sync | Marketing Cloud Connect | Native connector: Contacts, Leads, Accounts, Opportunities in; opt-outs written back to every Contact and Lead | | Where it runs | Salesforce's cloud | Plainsend's cloud, data in Sydney. Your own AWS account is coming | ## Is Marketing Cloud Next a forced migration? **No.** Salesforce says Engagement has no end-of-life, end-of-sale or end-of-roadmap plan, and you don't need to migrate to start using Marketing Cloud Next ([Engagement pricing](https://www.salesforce.com/marketing/engagement/pricing/), read 2026-10-12). Next arrives through the "+" editions at renewal. What does change at that renewal: Salesforce's own guidance suggests a 12-month, three-phase training plan in Data 360 and Flow Builder for Engagement teams moving to Next ([Salesforce blog](https://www.salesforce.com/blog/your-roadmap-to-marketing-cloud-next/), 2026-09-24), and some Engagement features (AMPscript, plain-text email, forms) are only now reaching Next ([SalesforceBen](https://www.salesforceben.com/top-9-summer-26-updates-for-salesforce-marketers/), 2026-04-24). If you're going to re-skill anyway, it's a fair moment to compare. ## What Plainsend doesn't do Be clear about this before you talk to us: - No advertising audiences, social publishing, landing pages or CMS. - No SMS yet (coming, Australia first), and no mobile push or WhatsApp. - No send-time optimisation, predictive scoring or AI content generation built in. Your own agent can write copy; Plainsend checks it. - No SOC 2 report or penetration test yet. See [security](https://plainsend.app/security.md) for what we do have. - A small company. You'll be talking to the people who build it. ## Prices at a glance | Edition | Per month (list, billed yearly) | Includes | |---|---|---| | Engagement Pro+ | $2,000 | 2.5M emails, 15,000 contacts, Marketing Cloud Growth | | Engagement Corporate+ | $5,500 | 10M emails, 1M mobile messages, 45,000 contacts, Marketing Cloud Advanced | | Engagement Enterprise+ | $30,000 | 150M emails, 10M mobile messages, 500,000 contacts, Marketing Cloud Advanced | | Intelligence+ add-on | from $11,000 | | | Personalization+ add-on | from $15,000 per org | | Sources: [Engagement pricing](https://www.salesforce.com/marketing/engagement/pricing/) (read 2026-10-12); [Salesforce blog, next-gen Marketing Cloud](https://www.salesforce.com/blog/next-gen-marketing-cloud-details/) (2025). Run your own numbers in the [cost calculator](https://plainsend.app/calculator.md). [Plan a switch](https://plainsend.app/switch.md) · [Cost calculator](https://plainsend.app/calculator.md) --- # Plainsend for agents Everything in Plainsend is an API call, a CLI command and an MCP tool. Your agent can import contacts, write templates, build segments and journeys, preview and simulate them, and run test sends. Anything that reaches real customers waits for a person to approve it. ## Paste this prompt Give this to Claude Code, Claude, or any agent that can use MCP: ```text Set up Plainsend for me. 1. Read https://plainsend.app/llms.txt and https://plainsend.app/docs/agents.md. 2. Connect to the Plainsend MCP server at https://api.plainsend.app/mcp (OAuth; I will approve the connection). 3. Call workspace_get and tell me which workspace you are in and what your key may do. 4. Help me import my contacts as a dry run first, then build a welcome journey from the gallery. Draft everything; never claim something is live until approval_status says approved. ``` ## Connect the MCP server **Claude (claude.ai):** Settings → Connectors → Add custom connector → URL `https://api.plainsend.app/mcp`. No client ID or secret. Claude registers itself, and an owner of the workspace approves the connection once. **Claude Code, with an agent key** (an owner creates one in the console, or with `plainsend keys create claude --kind agent --label "Claude for Sam"`): ```sh claude mcp add --transport http plainsend https://api.plainsend.app/mcp \ --header "Authorization: Bearer ps_..." ``` **Any other MCP client:** point it at the same URL. It discovers OAuth from the first response, or send an API key as a bearer token. The full tool list is in the [MCP reference](https://plainsend.app/docs/mcp.md). ## What an agent may do | An agent key can | It can't | |---|---| | Read everything except raw source rows | Erase contacts or change workspace settings | | Draft brands, templates, sources, segments and journeys | Add sending domains or manage keys | | Lint, preview, simulate and preflight | Approve its own requests | | Import files and run syncs (dry run by default) | Send to anyone outside your verified team without approval | | Send tests to verified team addresses | | | Ask to activate a journey or send a broadcast | | When an agent asks to go live, it gets back an approval id. An owner approves it in the console ("Needs you"), from the CLI, or from a one-time link. Approval is part by part: a broadcast's audience, message and send settings; a journey's entry, steps and goal. If the agent changes something after a part was approved, that part resets and the owner sees what changed. ## The CLI ```sh plainsend login --key ps_... --url https://api.plainsend.app plainsend setup # the first-run checklist: what's done, what's next plainsend workspace plan ./plainsend # what applying the folder would change plainsend workspace apply ./plainsend # save as drafts, ask for one go-live approval plainsend simulate welcome --contact plainsend approvals list ``` `plainsend --help` lists every command. Add `--json` for machine-readable output. **Install and sign in.** `curl -fsSL https://plainsend.app/install | sh` installs the CLI (Node.js 22 or later). `plainsend login` signs you in through the browser, so no key is pasted anywhere, and `plainsend init` sets a project folder up for your agent (instructions, a Claude skill, the MCP command) and sends a test email to you. **Coming:** The CLI is not on npm yet: until it is, the [install script](https://plainsend.app/install) says so and points you to the MCP server. ## Marketing as code Keep your workspace as files in your own repository: brands, templates (MJML or HTML), segments, sources and journeys as YAML. Every change is a reviewable diff, and `workspace plan` shows exactly what applying it would do, in dependency order. See [Working with Plainsend](https://plainsend.app/docs/agents.md) for the folder layout. ## Docs your agent can read - [llms.txt](https://plainsend.app/llms.txt): an index of every page as Markdown - [llms-full.txt](https://plainsend.app/llms-full.txt): every page in one file - Every page on this site has a Markdown copy: add `.md` to the address (this one is [/agents.md](https://plainsend.app/agents.md)) - Inside MCP, the same reference docs are resources: `plainsend://docs/agents`, `plainsend://docs/journeys`, `plainsend://docs/templates`, `plainsend://docs/mappings` --- # Quickstart From a new account to your first real send, in about 15 minutes if you can sign in to your DNS host. The console's **Set up** page is this checklist: it opens when you first sign in, ticks each step when it is really done (not when you click), and says what is still blocking email to your customers. Every step has two versions: **in the console** if you'd rather click, and **with your agent** if you'd rather ask. Mix them as you like: they use the same API, and `plainsend setup` or the MCP tool `setup_status` shows the same checklist. Three things only a person can do: pay, sign in to your DNS host (or send a link to whoever can), and approve the first real send. ## 1. Sign up and send a test to yourself ### In the console [Create an account](https://app.plainsend.app/auth/login?signup=1) and name your workspace. It starts in a sandbox: email goes only to you and your team, nobody else. On **Set up**, press **Send me a test**: a ready-made hello lands in your inbox in under a minute. ### With your agent Sign in from a terminal, then send the test: ```sh plainsend login plainsend send --test ``` Or connect the MCP server (see [agents](https://plainsend.app/agents.md)) and ask: ```text Call setup_status and tell me the next step. ``` **Coming:** The CLI is not on npm yet: until it is, use the console or the MCP server. ## 2. Your business details ### In the console **Set up → Your business details**: your postal address, sender name and where replies go. The law requires a postal address in marketing email, so Plainsend sends nothing to customers until you add it. ### With your agent ```sh plainsend setup details --postal-address "1 Harbour St, Sydney NSW 2000" --reply-to you@yourcompany.com ``` ```text Set our brand's postal address to
and its reply-to to with brand_put. ``` ## 3. Pick a plan ### In the console **Settings → Billing**: Solo is $12 a year, Starter $29 a month. Paying is an owner's job: agents can read your plan and usage, never buy. **Coming:** Checkout is opening soon. Until then, a new workspace stays in the sandbox: you can set everything up and send to your team. ### With your agent ```sh plainsend billing plainsend billing checkout solo ``` ```text Check our plan with billing_status and tell me what I need to buy. ``` ## 4. Verify your sending domain ### In the console **Set up → Sending domain**. Use a subdomain such as `mail.yourcompany.com`. With many DNS hosts Plainsend adds the records after you sign in there (Domain Connect); on Cloudflare it can add them with a token. Elsewhere, copy them, or send the setup link to whoever runs your DNS. The page checks each record live and says what's wrong in plain words. Once you have paid and the domain is verified, your workspace leaves the sandbox: then change your From address to the new domain. ### With your agent An owner adds the domain (agent keys can't), then the agent tracks it: ```sh plainsend domain add mail.yourcompany.com plainsend domain share mail.yourcompany.com ``` ```text Check domain_status for mail.yourcompany.com and tell me which records are still missing. ``` ## 5. Bring your contacts ### In the console **Sources → Add a source**: upload a CSV or Excel file, add a form on your website (or send contacts from your app through the API), connect HubSpot or Salesforce (Growth plan and up), or fill in a Databricks connection (Enterprise). A file import shows each column, a suggested use and a dry run before anything is written. Plainsend never guesses consent: you confirm a consent column, or say how people agreed. ### With your agent ```sh plainsend sources add csv contacts.csv ``` ```text Upload contacts.csv with source_upload, show me the dry run with source_upload_plan, ask me how people agreed to hear from us, then import it with source_upload_import. ``` ## 6. Pick a starter journey ### In the console **Content → Gallery** has five packs: clinic starter, win-back, monthly newsletter, tax time, and software trial to paid. Each one checks your account first (which properties and events it needs) and previews every email in your brand. It installs as drafts: nothing sends until you approve it. ### With your agent ```text Search the gallery, check the best fit against my account with gallery_show, and install it as drafts. Then simulate the journey for a test contact. ``` ## 7. Approve your first real send ### In the console Open **Needs you** (or the last step of Set up). The request shows who will get it, who won't and why, and each email. Approve each part, or all of it. A new domain warms up: a daily cap that rises over about three weeks, most engaged contacts first. Your first email to a customer from your own domain finishes Set up. ### With your agent ```text Request activation for the journey and give me the approval link. Then watch approval_status and report_health and tell me how the first sends went. ``` ## Reference - [Working with Plainsend (for agents)](https://plainsend.app/docs/agents.md): keys, marketing as code, the loop, the rules - [Journey documents](https://plainsend.app/docs/journeys.md): triggers, steps, goals and versions - [Templates](https://plainsend.app/docs/templates.md): MJML or HTML with Liquid, checks and brand kits - [MCP server](https://plainsend.app/docs/mcp.md): connecting and the tool list - [For agents](https://plainsend.app/agents.md): the prompt to paste, the CLI and `llms.txt` --- # Working with Plainsend (for agents) Plainsend is a headless marketing platform: contacts with consent, email templates, journeys and broadcasts, all driven by API, CLI (`plainsend`) or MCP. You work like a developer: draft, check, then ask a person to approve what reaches customers. ## Your key `workspace_get` says which account you act on and your key's kind. In a new workspace, start with `setup_status`: the first-run checklist (test to yourself, business details, plan, domain, contacts, a starter journey, the first approved send), each step with its state, what is missing, the tools that do it and whether only a person can. An **agent** key may read everything (except raw source rows), draft brands, templates, sources and journeys, preview, simulate, preflight, test-send and import data. It may not erase contacts, change settings, add sending domains or manage keys (refused with `forbidden` and a fix), and **activating a journey or sending a broadcast becomes an approval**: you get `{ approvalRequired: true, approvalId, consoleUrl }`, an owner approves it (console "Needs you", or `plainsend approvals approve `), and `approval_status` tells you the outcome. Never claim something went live until `approval_status` says `approved`. An owner approves a request part by part (a broadcast's audience, message and send settings; a journey's entry, steps and goal; checks run by themselves). If you change something after its part was approved (a new template version, a new draft), that part resets and the owner sees what you changed: say so when you change it. `log_search` shows who did what, people and agents alike. ## Marketing as code Keep the account's resources as files in the customer's repo and apply them, so every change is reviewable: ``` plainsend/ settings.yaml (tracking, utm, frequencyCap: owner keys only) domains.yaml (a list of {domain, brand?}: owner keys only) brands/.yaml brand_put (name, fromEmail, fromName, postalAddress, subscriptionTypes) sources/.yaml source_put (connector, mapping or mappingFile, consentSource, identity, credentialsFromEnv: {field: SECRET_NAME}) blocks/.mjml|.yaml block_put brandkits/.yaml brandkit_put templates/.mjml|.html template_put (YAML front matter: brand, subject, textBody?) segments/.yaml (the segment document) journeys/.yaml journey_put (saves a draft; activation is separate) ``` The folder is one **workspace bundle**: `workspace_plan` shows, in dependency order, what applying it would create, change or leave alone, with each resource's diff and issues (references inside the bundle resolve before the resources exist). `workspace_apply` saves everything as drafts, stops at the first error with what was and was not applied, and asks an owner for one **go-live** approval (a part per journey, and the first broadcast if the bundle has one). `workspace_export` returns the workspace as a bundle without secrets. `plainsend workspace plan|apply|export [dir]` does the same from a terminal (`plainsend plan|apply ` too). Saving unchanged content is a no-op, so applying twice is safe. **Gallery packs** are ready-made bundles (clinic starter, win-back, monthly newsletter, tax time, software trial to paid). `gallery_search` lists them (category, industry, words); `gallery_show` checks one against the account (which contact properties and events it has or lacks, and how to provide each) and previews every template in the brand's kit; `gallery_install` applies it as drafts with keys the account already uses renamed (`welcome` → `clinic-welcome`), and its journeys go live through the same go-live approval. Pass `plan: true` to see the workspace plan first. ## The loop 1. **Data**: for a sheet, `source_upload` (columns and suggested choices), `source_upload_plan` with the choices (a dry run: counts and rejected rows with fixes), then `source_upload_import`. Or `source_put` a files source with a mapping (plainsend://docs/mappings) and `source_import_file`. `source_quarantine` lists rejected rows. Consent needs the owner's word: a column they confirmed, or their statement of how people agreed; never invent one. A CRM is connected by a person: give them the `source_connect_link`. 2. **Content**: `template_lint` (fix every error; read the warnings), `template_put`, then `template_preview` as a real contact (plainsend://docs/templates). 3. **Journey**: `journey_validate`, `journey_put`, `journey_plan`, `journey_simulate` (plainsend://docs/journeys). 4. **Ask**: `journey_request_activation` or, for a one-off send, `send_preflight` then `send_request_broadcast`. 5. **Watch**: `journey_funnel`, `enrolment_trace`, `report_sends`, `report_health`, `contact_get` (consent and recent skips: why someone did not get an email). 6. **Outcomes**: `goal_put` makes an event a goal (default: the last human click within 7 days gets the credit); `report_attribution` says what emails led to, per campaign, content, journey step or broadcast. Quote its `rule` next to every number you report. ## Rules that never bend - Every message goes through the eligibility check (consent, suppression, frequency cap, sandbox allowlist, verified sending domain, the reputation guard). No key can bypass it; a refusal is a **skip** with a reason, not an error. A new self-serve workspace is in the sandbox: it sends only to its own members (`sandbox_team_only` otherwise) until it has paid and verified a sending domain. Until its brand has a real postal address, it sends only tests and mail to its members (`postal_address_missing` otherwise). - A new sending domain warms up: a daily cap rising from 50 over about three weeks (`domain_warmup`). Over the cap a message **waits** for a later day (most engaged contacts first), it is not skipped; preflight's `warm_up` step says how many. Big broadcasts send a first slice and hold the rest until its bounces are in. - The reputation guard pauses a brand whose bounce or complaint rate crosses the pause line (`report_health` shows the rates, lines and why). Its messages skip with `reputation_paused` until an owner resumes it (`reputation_request_resume` asks one). - Plans have limits (`billing_status`: plan, usage this month, limits). On Solo, or an account that is not paid up, new sends stop at 120% of the plan's monthly emails and skip with `plan_email_limit` (preflight's `plan_limit` step says how many); elsewhere going over only warns. Unsubscribed contacts are free. You cannot buy or change a plan: say what is needed and an owner does it in Settings → Billing. - Consent comes from data with evidence (a mapping's `consent`, the identify API), never from a guess. - Errors are `{ code, message, details, fix }`: read the fix and correct your input rather than retrying blindly. --- # Journey documents A journey is one document (JSON, or the same document as YAML), saved as an immutable **version**: `draft` → `active` (one per journey; each template's version is pinned at activation) → `draining` (keeps its enrolments, takes no new ones) → `stopped`. Saving the same document again makes no new version. The schema is `apps/api/src/modules/journey-document.ts`; a worked example is `examples/welcome/journey.yaml`. ```yaml key: welcome # 2-63 lowercase letters, digits and dashes; must match the URL / tool key name: Welcome series brand: main # a brand key subscriptionType: news # one of the brand's subscription types: the consent every send checks entry: on: consent_granted # enter when a person gains consent for brand + subscriptionType # on: { event: signed_up } # or: enter on a tracked event (they still need consent to be sent to) reentry: never # each person enters once, ever (the only option today) goal: { event: activated } # optional: the event that counts as success; exits them (exit: true) exitWhen: # optional, up to 20: leave as soon as one is true - { event: unsubscribed_everywhere } - { property: plan, equals: enterprise } start: welcome # the first step's key steps: welcome: { type: send, template: welcome, next: wait-2d } wait-2d: { type: wait, days: 2, next: clicked } # days, hours and/or minutes; at most 365 days clicked: type: branch if: { clicked: welcome } # a human click on the email sent at send step `welcome` then: next-steps else: nudge next-steps: { type: send, template: next-steps, next: done } nudge: { type: send, template: nudge, next: done } done: { type: exit } ``` ## Steps Step keys: lowercase letters, digits, dashes and underscores. Every step except `exit` names where to go next. | Type | Fields | What it does | |---|---|---| | `send` | `template`, `next` | Renders the template for the contact and sends it, if the eligibility check allows (consent, suppression, frequency cap, sandbox allowlist, verified sending domain, the reputation guard). A refusal is a **skip** with a reason; the enrolment still moves on. Over the sending domain's warm-up cap the step is **deferred** (`warm_up`): the enrolment waits at the step until the next day, then sends. | | `wait` | `days` / `hours` / `minutes`, `next` | Waits. Sandbox accounts divide waits by their time scale. | | `branch` | `if`, `then`, `else` | `if` is one of `{ clicked: }`, `{ event: , within: { days: 3 } }`, `{ property: , equals: }`. | | `split` | `percent`, `a`, `b` | Sends `percent`% of people to `a`, the rest to `b`. | | `update` | `properties`, `next` | Sets contact properties. | | `webhook` | `url` (https), `next` | POSTs the enrolment to your URL. | | `exit` | | Ends the enrolment. | ## Validation `journey_validate` (and every save) checks the schema, the graph and the references. Each error names the `step` (when there is one), the `field`, the `rule` and a `fix`: | Rule | Meaning | |---|---| | `invalid_field`, `unknown_step_type` | The document does not match the schema (unknown fields are errors). | | `key_mismatch` | The document's `key` differs from the key you saved it under. | | `unknown_step` | `start`, `next`, `then`, `else`, `a` or `b` points at a step that does not exist. | | `unreachable_step` | No path from `start` reaches the step. | | `cycle_without_wait` | A loop with no wait in it would run forever. | | `invalid_wait`, `wait_too_long` | A wait with no duration, or over 365 days. | | `clicked_not_a_send` | A `clicked` branch names a step that is not a send. | | `webhook_not_https` | Webhook URLs must be https. | | `unknown_brand`, `unknown_subscription_type`, `unknown_template` | A reference that does not exist yet: create it first. | | `template_brand_differs` (warning) | The template was written for another brand. | ## Before it goes live 1. `journey_plan`: the step diff against the active version, the template versions activation would pin, and how many people are in progress on the active version (they finish on it; nobody is migrated). 2. `journey_simulate`: the path a real contact (`contactId`) or sample data would take, with `events` (`afterDays`) and `clicked` send steps. Nothing is sent. 3. `journey_request_activation`: an agent key gets `{ approvalRequired, approvalId, consoleUrl }` and an owner approves; an owner key activates. The previous active version drains. 4. Afterwards: `journey_funnel` per step, `enrolment_trace` for one person (why they did or didn't get each email). --- # Plainsend MCP server Customers' agents (Claude Code, claude.ai, any MCP client) drive Plainsend through `https://api.plainsend.app/mcp`, served by the API Worker itself (streamable HTTP). Code: `apps/api/src/mcp/`. Tests: `apps/api/test/mcp.test.ts`, `apps/api/test/cli-login.test.ts`, `apps/api/test/mcp-oauth.test.ts`. ## Connect **Start here (a project folder):** install the CLI, sign in, and let `init` set the folder up: ```sh curl -fsSL https://plainsend.app/install | sh # or npm i -g @plainsend/cli (packages/cli; not published yet) plainsend login # browser sign-in; --agent for a drafts-only agent key plainsend init [--pack ] # plainsend.config, AGENTS.md section, .claude skill, test email to you claude mcp add --transport http plainsend https://api.plainsend.app/mcp ``` **Claude Code, with OAuth**: the `claude mcp add` line above, with no header. Claude Code registers itself and opens the consent page, like claude.ai below. **Claude Code, with an API key** (an owner creates an agent key: `plainsend keys create claude --kind agent --label "Claude for Brendan"`): ```sh claude mcp add --transport http plainsend https://api.plainsend.app/mcp \ --header "Authorization: Bearer ps_..." ``` **claude.ai (custom connector), with OAuth**: Settings → Connectors → Add custom connector → URL `https://api.plainsend.app/mcp`, no client ID or secret. Claude registers itself and opens Plainsend's consent page. With WorkOS sign-in configured (production), the page runs on the console's host (`app.plainsend.app`, where the session cookie lives). A person signed in there approves, and the connector gets a new agent key in their current workspace labelled "Claude for " (only an owner or admin may). Signed out, the page offers "Sign in with your Plainsend account" (`/auth/login?return_to=`) or an **owner** key pasted once with a name (used for two calls and never stored); without WorkOS only the owner key. A person signed in with no workspace yet is sent to "Name your workspace" (`/?return_to=`) and comes back to the page once it exists. Revoke the connector by revoking that key (`plainsend keys list`, `plainsend keys revoke `); its next call gets a 401 and Claude asks to connect again. **The CLI (`plainsend login`)**: OAuth 2.1 with PKCE as the static public client `plainsend-cli` (stored on first use by `src/mcp/oauth.ts`; redirect `http://127.0.0.1/callback`, any port, RFC 8252), on a random loopback port. Scope `person` (the default; the CLI skips the owner-key form and goes straight to sign-in, and "Name your workspace" when there is none): the consent page starts a CLI person session (`person_sessions.via = 'cli'`) and the token acts as the person in their role, re-checked with WorkOS like a console session, audited as `person: via cli`; it lasts 30 days. Scope `agent` (`--agent`): an agent key labelled "Plainsend CLI for ". Over SSH, `--no-browser` prints the address and takes the pasted callback address (the provider has no device-code grant, and this needs nothing more from it). Tokens are stored in `~/.config/plainsend/config.json` (0600) and refreshed by the CLI. `plainsend logout` calls `DELETE /v1/session`, which ends the session and so every token from it. Any other client: point it at the same URL. It discovers OAuth from the 401 (`WWW-Authenticate: Bearer ... resource_metadata="https://api.plainsend.app/.well-known/oauth-protected-resource/mcp"`), or send an API key as a bearer header. ## What an agent can do Every tool calls the REST route it mirrors, in-process, with the caller's key (`src/mcp/api.ts`), so `authorize()` decides exactly as for REST (`src/http/access.ts`, `src/modules/policy.ts`). With an agent key: - Reads, drafts, previews, simulations, preflights, test sends, imports and syncs run. - `journey_request_activation` and `send_request_broadcast` return `{ approvalRequired: true, approvalId, consoleUrl, action, target, next }` (not an error); an owner approves in the console ("Needs you"), with `plainsend approvals approve [--part ]` or a one-time approve link sent to Slack; `approval_status` follows it, part by part (a broadcast's audience, message, send settings and checks; a journey's entry, steps, goal and checks). A part the agent changes after an owner approved it resets ("changed", with the diff since) and needs a new OK. - Anything else the agent kind may not do (`domain_add`, raw quarantine rows) is refused: `isError: true` with `{ error: { code: "forbidden", message, details: { action, keyKind }, fix } }`. With an owner key the same tools act directly (an activation activates). There is deliberately no tool to approve or reject, erase contacts, change settings, manage keys or enrol by hand. | Tool | REST | | Tool | REST | |---|---|---|---|---| | `workspace_get` | `GET /v1/me`, settings, brands | | `journey_list` | `GET /v1/journeys` | | `brand_list` / `brand_put` | `GET /v1/brands`, `PUT /v1/brands/:key` | | `journey_get` | `GET /v1/journeys/:key` (+ the version's document) | | `template_list` / `template_get` | `GET /v1/templates[/:key]` | | `journey_validate` / `journey_put` | `POST .../validate`, `PUT /v1/journeys/:key` (object or YAML) | | `template_lint` / `template_put` | `POST /v1/templates/:key/lint`, `PUT /v1/templates/:key` (both return `issues`) | | `journey_plan` | `GET /v1/journeys/:key/plan` | | `template_preview` | `POST /v1/templates/:key/preview` | | `journey_simulate` | `POST /v1/journeys/:key/simulate` | | `template_test_send` | `POST /v1/sends/test` | | `journey_request_activation` | `POST .../versions/:v/activate` (default: newest draft) | | `source_list` / `source_put` | `GET /v1/sources`, `PUT /v1/sources/:key` | | `journey_funnel` | `GET /v1/reports/journeys/:key/funnel` | | `source_import_file` | `POST /v1/sources/:key/runs` (base64 CSV/XLSX, dry run by default) | | `enrolment_trace` | `GET /v1/enrolments/:id` (or a contact's latest) | | `source_sync` | `POST /v1/sources/:key/sync` (dry run by default) | | `send_preflight` | `POST /v1/sends:preflight` | | `source_runs` / `source_quarantine` | `GET /v1/sources/:key/runs`, `/quarantine` | | `send_request_broadcast` | `POST /v1/sends` | | `source_upload` | `POST /v1/sources/:key/uploads` (columns, suggested choices, first rows) | | `source_upload_plan` | `POST .../uploads/:id/plan` (dry run, writes nothing) | | `source_upload_import` | `POST .../uploads/:id/import` (a run, paged) | | `source_connect_link` | `GET /v1/sources/connect/:provider/start` (a sign-in link for a person) | | `contact_find` | `GET /v1/contacts` (`email` or `q`) | | `report_sends` | `GET /v1/reports/sends[/:id]` | | `contact_get` | `GET /v1/contacts/:id` + skipped sends + enrolments (the why-not) | | `approval_status` | `GET /v1/approvals[/:id]` | | `domain_add` | `POST /v1/domains` (owner only) | | `domain_status` | `GET /v1/domains[/:domain]`, `POST .../check` | | `domain_warmup` | `GET /v1/domains/:domain/warmup` | | `report_health` | `GET /v1/reports/health` (`window` 24h/7d/30d) | | `reputation_request_resume` | `POST /v1/brands/:key/reputation/resume` (agent: an approval) | | `billing_status` | `GET /v1/billing` (read only; paying is an owner's, in the console) | | `log_search` | `GET /v1/activity` (actor, kind, since, cursor) | | `setup_status` | `GET /v1/setup` (the first-run checklist, what blocks sends; read only) | | `workspace_plan` | `POST /v1/workspace:plan` (a whole bundle, in dependency order) | | `workspace_apply` | `POST /v1/workspace:apply` (drafts; go-live is one approval) | | `workspace_export` | `GET /v1/workspace:export` (no secrets) | | | | Tool names use underscores, not the dots of the ticket (`journey.plan`): the Claude API's tool-name pattern is `^[a-zA-Z0-9_-]{1,64}$`. Results carry a one-line summary and the JSON (as text and as `structuredContent`). Errors keep the API's `{ code, message, details }` and always add a `fix` (from the details when the route gave one, else per code: `src/mcp/result.ts`). **Resources** (read-only, bundled at build time by `scripts/build-mcp-docs.ts` into `src/mcp/docs.generated.ts`; run `pnpm mcp:docs` after editing the sources, a test fails while the bundle is stale): `plainsend://docs/agents` (`docs/reference/agents.md`), `plainsend://docs/mappings` (`docs/mappings/README.md` + `sheet.yaml`), `plainsend://docs/journeys` (`docs/reference/journeys.md`), `plainsend://docs/templates` (`docs/reference/templates.md`). Segments have no document yet, so no segment reference. **Prompts**: `setup_from_spreadsheet { file, brand? }`, `build_welcome_journey { brand, subscriptionType }`. --- # Templates: MJML or HTML with Liquid A template is an email's subject, body and optional plain-text body, saved as an immutable **version** (saving the same content again makes no new version). `format: mjml` (MJML 5) compiles once on save (Liquid is kept for send time); `format: html` is cleaned and wrapped in the brand kit (below). Every save runs the **checks** first: errors refuse it, warnings are saved with the version. Examples: `examples/welcome/templates/`. ```yaml brand: main format: mjml subject: "Welcome, {{ contact.first_name | default: 'there' }}" body: | Your account is ready Hi {{ contact.first_name | default: 'there' }}, Thanks for joining {{ brand.name }}. Get started textBody: null # optional; derived from the HTML when null utm: false # optional; overrides the account's UTM setting for this template ``` ## Importing a design (Claude Design, HTML, MJML, a web page) `POST /v1/templates/import` (MCP `template_import`, `plainsend template import --brand --key `, the console's Content → Import) turns a design into an MJML template in the brand's kit: - **In**: a Claude Design export `.zip` (its `index.html`, stylesheets and images; an `.mjml` file in a zip wins), an HTML file, MJML, or a public web page (`url`). JSON (`file: {name, content: }`, `html`, `mjml` or `url`) or multipart (`file`). Options: `brand` (needed when the workspace has several), `key` (else made from `name` or the title), `name`, `subject` (else the page title or first heading), `save: false` to only look. A zip whose page is built by JavaScript when it opens (a React app) is refused: export it as standalone HTML. PDFs are refused. - **Converted**: styles are computed (stylesheets, specificity, `var()`, inheritance) and written inline; each block becomes a section named after its class (so UTMs say where a link sat); a flex or grid row of 2–4 items, or a table row of 2–4 cells, becomes columns (they stack on phones); 5 or more stack. Headings keep their level, a link styled as a button becomes `mj-button`. Removed: scripts, forms, frames (a YouTube embed becomes a thumbnail linking to the video), video (its poster, linking to it), SVG, animations, hover effects, pop-ups and fixed bars, site navigation and tracking pixels. Web fonts are kept with an email-safe fallback; background images become a background colour (a warning). With a brand kit, the design's header and footer are replaced by the kit's; colours and fonts equal to the kit's become brand tokens. MJML passes through as written. - **Images** are copied into your asset store and served from `https://api.plainsend.app/a//.`; an image without alt text gets `[describe image]` and a warning. - **Personalisation**: `[First name]`, `{{first_name}}`, `*|FNAME|*`, `%%FirstName%%` become `{{ contact.first_name | default: 'there' }}`; Mailchimp's `*|UNSUB|*`, `*|LIST:ADDRESS|*`, `*|LIST:COMPANY|*` become the unsubscribe link and the brand's address and name. An unknown property is an `unknown_property` error. - **Out**: `{key, version, created, saved, subject, mjml, fields, changes, issues, previews: {desktop, mobile}, next}`. `fields` are what to edit next (headline, text, buttons with their links, images, repeated cards as one list, and personalisation); `changes` say what was changed, in plain English; `issues` are the checks below plus the import's own warnings. It is saved as a new version only when there are no errors; otherwise fix `mjml` and `PUT` it. ## Liquid [Liquid](https://shopify.github.io/liquid/) with strict filters (an unknown filter is an error on save). | Variable | Value | |---|---| | `contact.email`, `contact.first_name`, `contact.last_name`, `contact.phone`, `contact.country`, `contact.region`, `contact.locale`, `contact.id` | The contact's core fields (snake_case). | | `contact.` | A custom property from a source mapping's `properties`, or one already in your contacts' data, e.g. `contact.postcode`. | | `event.` | The triggering event's payload, in event-triggered journeys. | | `brand.name`, `brand.key`, `brand.from_email`, `brand.from_name`, `brand.postal_address` | The sending brand. | | `unsubscribe_url` | This message's unsubscribe link (one is always added in the footer anyway). | Values are HTML-escaped in the body, not in the subject or text. Two rules are checked on every save (ADR-0005): - **Every output has a fallback**: `{{ contact.first_name | default: 'there' }}`. Exempt: `contact.email`, `contact.id`, `brand.*`, `unsubscribe_url`, loop items, and properties marked `required: true` in their mapping (`properties: { plan: { from: Plan, required: true } }`). Use single quotes inside MJML attributes. `POST /v1/templates//suggest-fallbacks` (body: `properties: ["contact.first_name"]` and/or a draft's `subject`/`body`) proposes one per property type (first name → "there", numbers → 0) with the replacement text. - **Every property is known**: an unknown one (`contact.frist_name`) is an error whose fix names the closest match. ## Conditions and loops (MJML) Liquid control flow in MJML goes through two tags; never hand-write `{% if %}` pairs. ```xml … ``` - `when`: properties, strings ('pro'), numbers, `true`/`false`/`blank`/`empty`; `== != > < >= <= contains`; clauses joined by `and`/`or` (no parentheses); filters `days_ago`, `days_until`, `size`, `downcase`, `upcase`, `abs`. - `each`: a list property (the item is `item` unless named); `limit` defaults to 10, at most 50. - Any other `ps-*` tag is an error. In HTML templates use Liquid `{% if %}` directly. ## HTML templates HTML (an import, a Claude Design export) is cleaned, then wrapped in the sending brand's kit like MJML. Removed, in one warning: script, iframe, object, embed, forms and inputs, `on…=` attributes, `javascript:` links, external stylesheets and `@import`, 1x1 or hidden tracking pixels. Kept: inline styles, tables, Outlook conditional comments, Liquid. It is refused when unsafe structure remains (an unclosed `/lint` (MCP `template_lint`, `plainsend template lint `) runs every check without saving; `PUT` (MCP `template_put`, `plainsend template publish `) runs them and returns `issues`; `GET /v1/templates//versions//issues` reads a version's. Each issue is `{ field, line, rule, severity, message, fix }`; `line` is the line in that field. | Rule | Severity | What | |---|---|---| | `liquid`, `mjml`, `unknown_tag`, `ps_if`, `ps_for`, `unclosed_tag`, block rules | error | the source does not parse or compile | | `missing_fallback`, `unknown_property` | error | the ADR-0005 lint above | | `unsafe_html` / `html_removed` | error / warning | HTML cleaning | | `unsubscribe`, `postal_address` | error | the footer's unsubscribe link and address are missing or hidden as sent | | `gmail_clip` | warning over 90 KB, error over 102 KB | Gmail clips the email (and its footer) | | `img_alt` | warning | an image without alt text | | `broken_link` | warning | a link answers 4xx/5xx or not at all (each URL once; Liquid links skipped) | | `outlook_css` | warning | flex, grid or position, which Outlook ignores | | `dark_mode_logo`, `dark_mode_text` | warning | a transparent logo without `logo.darkUrl`; pure black text | | `subject_length`, `preview_text` | warning | over 60 characters; no `` (or hidden preheader in HTML) | ## Added at send time (you cannot remove them) - A footer with the brand name, postal address and an unsubscribe link, in the HTML and the text. - `List-Unsubscribe` and `List-Unsubscribe-Post` (one-click) headers. - Click tracking (every `http(s)` link is rewritten to a signed redirect) and an open pixel, unless the account turned tracking off. - UTM parameters on every tracked link that has none: `utm_source=plainsend`, `utm_medium=email`, `utm_campaign=`, `utm_content=, header, footer, or body>`. Off for the account with `PATCH /v1/tenant/settings {"utm": false}`, per template with `utm`. The links as sent (with their UTMs) are stored on the send (`GET /v1/sends/` → `links`) and shown by `template_preview`. ## Check before sending - `template_preview` renders exactly what a contact (or sample data) would receive. Nothing is sent. - `template_test_send` sends one test through the normal eligibility check: in a sandbox or test account only verified recipients receive it; anyone else is skipped with a reason (`contact_get` shows it). - `template_lint` before saving; errors on save list every issue with its field, line and fix. - `POST /v1/templates//preview` with `draft: {brand, subject, body, format}` renders an unsaved source in its brand's kit (`version: 0`; errors refuse it like a publish). The console's builder previews with it. - `GET /v1/properties` lists every property a template may use (`contact.*` with its type, and whether it is required, so needs no fallback). ## Brand kits: one template, any brand's look Each brand can have a **brand kit** (`PUT /v1/brandkits/`, MCP `brandkit_put`). Templates hold content only; the renderer wraps an MJML template in its sending brand's kit, so the same template renders in any brand's kit (`template_preview` with `brand` shows it in another brand's kit; a journey sending as another brand uses that kit). ```yaml colors: { primary: "#0e5e6f", accent: "#f2a541", text: "#1d2a30", background: "#f6f4ef" } # dark-mode colours are generated fonts: heading: { family: Fraunces, url: "https://fonts.googleapis.com/css2?family=Fraunces:wght@600&display=swap" } body: { family: Inter } # fallback chosen for you (Arial, Helvetica, sans-serif); or set `fallback` logo: { url: "https://example.com/logo.png", darkUrl: "https://example.com/logo-white.png", alt: "Harbour Physio" } button: { radius: 6, background: accent } # primary | accent | a hex colour; text colour picked for contrast layout: { width: 600, header: "…", footer: "" } footer: { text: "You're a patient of {{ brand.name }}.", preferencesUrl: "https://example.com/preferences" } ``` - The kit sets MJML defaults (`mj-text`, `mj-button`, fonts) and dark-mode CSS; use `mj-class="ps-heading"` for headings. Brand tokens resolve at compile: `{{ brand.color.primary }}`, `{{ brand.color.accent }}`, `{{ brand.font.heading }}`, `{{ brand.logo.url }}`, `{{ brand.button.radius }}`. - The footer always carries the brand name, postal address and unsubscribe link; the kit only adds to it. - Text must reach 4.5:1 on its background (light and dark) or the put is refused; pale brand colours are warnings. - Before changing a kit, `POST /v1/brandkits//plan` (MCP `brandkit_plan`) lists every template of the brand rendered before and after. `POST /v1/brandkits/from-url` (MCP `brandkit_from_url`) drafts a kit from a website; it saves nothing. - `POST /v1/brands//copy {toBrand, templates}` (MCP `brand_copy`) copies the kit and templates; each copy records where it came from. ## Shared blocks A **block** (`PUT /v1/blocks/`, MCP `block_put`) is an MJML fragment (sections) with declared fields: ```yaml name: Book a visit button fields: [{ key: label, default: "Book a visit" }, { key: url, type: url, required: true }] mjml: {{ field.label }} ``` Use it in a template as ``. Leave the version off (`ref="cta"`) and publishing pins the newest one. A new block version never changes a template: `GET /v1/blocks` and `GET /v1/templates/` (`blockUpdates`) list templates with an update available; name the new version to take it. --- # Security and data residency What we do today, and what we don't have yet. If your security review needs something that isn't here, [ask us](https://plainsend.app/switch.md#talk). ## Where your data lives | What | Where | |---|---| | Contacts, consent, events, templates, journeys | Postgres on Neon, in AWS Sydney (ap-southeast-2) | | Sending | Amazon SES in Sydney (ap-southeast-2), one SES tenant per workspace | | The API, console and MCP server | Cloudflare Workers. Requests are handled on Cloudflare's network, placed near the database | | Images in your templates | Cloudflare R2 | Data at rest is stored in Sydney. Like every web service, requests travel over the internet to reach it, encrypted with TLS. ## Run it where you want **Coming:** **Bring your own Amazon SES**: Plainsend sends through your SES account using a role you create and can revoke, so the sending reputation and bill are yours. **Plainsend in your own AWS account (BYOC)**: the whole platform in your account, so your data never leaves it. Both are being built with design partners. [Talk to us](https://plainsend.app/switch.md#talk). ## Access and control - **Sign-in** through WorkOS. Invite your team by email; roles are owner, admin, member and viewer. - **API keys have kinds**: owner, agent, source (for systems that send contacts and events in) and read-only. Each key shows when it was last used, and can be revoked at once. - **Agents can't send on their own.** An agent key drafts; activating a journey or sending a broadcast becomes an approval that an owner gives, part by part. Changing something after approval resets that part. - **Everything is logged.** The activity log records who did what: people, agents, sources and Plainsend itself. - **Credentials you give us for a source** (a CRM token, a database password) are encrypted before they are stored. A Cloudflare token for DNS setup is used once and never stored. - **Erasure**: deleting a contact removes it everywhere, events included. ## Sending safety - Every message goes through one eligibility check: consent, suppressions, frequency caps, the sandbox, a verified sending domain, and the reputation guard. It fails closed: if it can't decide, it doesn't send. - New sending domains warm up with a daily cap. A brand whose bounce or complaint rate crosses the line is paused until an owner resumes it. - One-click unsubscribe (RFC 8058) on every marketing email. ## Sub-processors The services that handle customer data for us: | Service | What for | Where | |---|---|---| | Cloudflare | Runs the API, console, MCP server and this site; stores template images (R2); bot checks (Turnstile) | Global network | | Amazon Web Services (SES) | Sends your email and reports bounces and complaints | Sydney (ap-southeast-2) | | Neon | The Postgres database | AWS Sydney (ap-southeast-2) | | WorkOS | Sign-in and team invitations | United States | | Stripe | Payments and invoices | United States and others | We'll update this list before adding a new one, and customers on a [data processing agreement](https://plainsend.app/legal/dpa.md) are told first. ## What we don't have yet Better you hear it from us: - No SOC 2 report and no independent penetration test yet. Both come with our first enterprise contracts. - No public status page yet. - No single sign-on (SAML or OIDC) for your own identity provider yet. - The legal documents ([terms](https://plainsend.app/legal/terms.md), [privacy](https://plainsend.app/legal/privacy.md), [DPA](https://plainsend.app/legal/dpa.md)) are drafts that a lawyer has not yet reviewed. ## Reporting a problem Found a security issue? Please tell us through the [contact form](https://plainsend.app/switch.md#talk) with "security" in the message, and don't test against other customers' data. We'll reply quickly. --- # Acceptable use policy > **Draft — not yet reviewed by a lawyer.** Last changed: 2026-10-13. Every Plainsend customer shares the reputation of the services we send through. One sender's spam can hurt everyone's delivery, so these rules are strict and we enforce them. ## Who you can email - Only people who gave you permission, or where the law allows you to email them without it (for example, in Australia, inferred consent under the Spam Act 2003 from an existing business relationship). Be ready to show where each contact came from. - **No purchased, rented, scraped or shared lists**, and no lists of people who haven't heard from you in years. - Honour every unsubscribe immediately. Plainsend does this for you; don't work around it (for example, by importing people again who unsubscribed). ## What you can't send - Anything illegal, or that promotes illegal activity. - Phishing, malware, or messages pretending to be someone else. - Misleading subject lines or sender names. - Content that harasses, threatens or promotes hate or violence against people. - Sexually explicit material. - Get-rich-quick schemes, multi-level marketing recruitment, unlicensed financial advice, or payday loan offers that break local lending law. - Unlicensed or prescription-only drugs, or anything that needs a licence you don't have. ## How you use the service - Don't try to get around sending limits, warm-up caps, approvals or the eligibility check, for example by opening several accounts. - Don't probe, scan or attack Plainsend or other customers' data. To report a security issue, see [security](https://plainsend.app/security.md). - Don't let agents you connect do what you couldn't do yourself. ## Thresholds We pause sending for a brand whose bounce or complaint rate crosses the reputation guard's line (about 2% bounces or 0.08% complaints over a rolling window), and tell you why. Mailbox providers expect complaint rates well under 0.3%. ## What happens if the rules are broken Depending on how serious it is, we may hold a send for review, pause sending, ask you to fix your list, or close the account. We'll tell you what we found and what you can do, unless doing so would help an attacker. Report abuse of Plainsend through the [contact form](https://plainsend.app/switch.md#talk). --- # Data processing agreement > **Draft — not yet reviewed by a lawyer.** Last changed: 2026-10-13. Words in [square brackets] are still to be filled in. This agreement ("DPA") forms part of the [terms of service](https://plainsend.app/legal/terms.md) between you ("Customer") and [Plainsend's legal entity and ABN] ("Plainsend"). It applies when Plainsend processes personal information about the Customer's contacts ("Customer Personal Data"). ## 1. Roles The Customer is the controller (or, under Australian law, the entity that collected the information). Plainsend is the processor: it processes Customer Personal Data only to provide the service and only on the Customer's documented instructions. The terms, the Customer's configuration (sources, journeys, sends) and its API calls are those instructions. ## 2. What is processed | | | |---|---| | Who | The Customer's contacts, leads and customers, and its staff who use Plainsend | | What | Contact details (email, phone, name), properties the Customer chooses to send, events, consent records, message and engagement history (deliveries, opens, clicks, bounces, complaints) | | Why | Storing contacts and consent, rendering and sending messages, running journeys, reports | | How long | For the term of the agreement, then as in section 8 | | Sensitive information | Not intended. The Customer should not send health, financial account or other sensitive information unless agreed in writing | ## 3. Plainsend's obligations Plainsend will: - process Customer Personal Data only on the Customer's instructions, and say if it believes an instruction breaks the law; - make sure its staff and contractors are bound to confidentiality; - keep appropriate technical and organisational security measures (section 5); - help the Customer answer requests from individuals (access, correction, erasure) through the API and console, which include contact export and erasure; - help the Customer with security assessments and notifiable data breach obligations, as reasonably needed; - tell the Customer without undue delay, and within [48 hours], after becoming aware of a breach affecting Customer Personal Data. ## 4. Sub-processors The Customer authorises the sub-processors listed on the [security page](https://plainsend.app/security.md#sub-processors). Plainsend will give at least [30] days' notice before adding or replacing one, and the Customer may object on reasonable grounds; if we can't resolve the objection, the Customer may end the affected service and get a refund of prepaid fees for it. Plainsend stays responsible for its sub-processors. ## 5. Security measures - Data at rest in Sydney (AWS ap-southeast-2); TLS in transit. - Credentials for the Customer's data sources encrypted before storage. - Access by scoped API keys and signed-in users with roles; agent keys cannot send or activate without an owner's approval. - An activity log of changes and sends. - Each workspace's data kept separate in the database, and a separate Amazon SES tenant per workspace. ## 6. Where data goes Customer Personal Data is stored in Australia. Some sub-processors may access it from other countries (see the list). Where the GDPR or UK GDPR applies, transfers are covered by [the EU Standard Contractual Clauses / UK addendum, to be attached]. ## 7. Audits Plainsend will give the Customer the information reasonably needed to show it meets this DPA. Plainsend does not yet have a SOC 2 report or independent penetration test; when it does, it will share them on request under confidentiality. [On-site audit rights to be agreed for enterprise contracts.] ## 8. End of the agreement When the agreement ends, the Customer can export its data for [30] days. Plainsend then deletes Customer Personal Data, except suppression records the Customer asks it to keep and anything the law requires it to retain. ## 9. Order of precedence If this DPA and the terms conflict about personal information, this DPA wins. --- # Privacy policy > **Draft — not yet reviewed by a lawyer.** Last changed: 2026-10-13. Words in [square brackets] are still to be filled in. This policy explains how [Plainsend's legal entity and ABN] ("Plainsend", "we") handles personal information. We follow the Australian Privacy Principles in the Privacy Act 1988 and, for people in the EU or UK, the GDPR. There are two kinds of personal information here: 1. **About our customers and visitors** (you, using this site or the console). We decide how it's used: this policy covers it. 2. **About our customers' contacts** (the people our customers email). Our customer decides how it's used, and we process it on their behalf under the [data processing agreement](https://plainsend.app/legal/dpa.md). If you received an email sent through Plainsend, the sender's privacy policy applies; every marketing email has an unsubscribe link. ## What we collect | What | When | Why | |---|---|---| | Name, email, company, what you use today, renewal month, message | You fill in the "talk to us" form | To reply to you | | Name, email, organisation, role | You sign up or are invited (through WorkOS) | To give you an account | | Billing details | You pay (through Stripe; we never see full card numbers) | To bill you | | What you do in the console and API (the activity log) | You use Plainsend | Security, audit and support | | Country of a form submission (from Cloudflare) | You use the form | To spot abuse | This site sets no tracking cookies and uses no analytics or advertising scripts. The form may use Cloudflare Turnstile to check you're not a bot. We don't sell personal information, and we don't use our customers' contact data for our own marketing. ## Where it is stored, and who helps us Personal information is stored in Sydney, Australia (Neon Postgres and Amazon SES, both in AWS ap-southeast-2) and on Cloudflare's network. Sign-in is handled by WorkOS and payments by Stripe, both of which may process information in the United States. The full list is on the [security page](https://plainsend.app/security.md#sub-processors). ## How long we keep it - Form messages: for [24 months] after our last contact with you, then deleted. - Account information: while your account is open, and for [30 days] after, then deleted, except invoices and other records the law requires us to keep. - Unsubscribe records: kept for as long as needed to make sure the person isn't emailed again. ## Your rights You can ask to see the personal information we hold about you, correct it, or delete it. If you're in the EU or UK you can also object to processing, restrict it, or ask for a copy in a portable format. Use the [contact form](https://plainsend.app/switch.md#talk); we'll reply within 30 days. If you're unhappy with how we handled your information, tell us first. You can also complain to the Office of the Australian Information Commissioner ([oaic.gov.au](https://www.oaic.gov.au)) or your local regulator. ## Changes If we change this policy in a way that matters, we'll say so on this page and, for customers, by email or in the console. ## Contact [Privacy contact to be confirmed.] Until then, use the [contact form](https://plainsend.app/switch.md#talk). --- # Terms of service > **Draft — not yet reviewed by a lawyer.** Last changed: 2026-10-13. Words in [square brackets] are still to be filled in. These terms are an agreement between you (the person or organisation using Plainsend, "you") and [Plainsend's legal entity and ABN] ("Plainsend", "we"). By creating a workspace or using the API, CLI or MCP server, you agree to them. ## 1. The service Plainsend is a platform for marketing and transactional messages: contacts, consent records, templates, journeys, broadcasts and reports, reached through a web console, an API, a command-line tool and an MCP server. We may change features over time. If we remove something you rely on in a way that matters, we'll tell you first. ## 2. Your account - You must give accurate details, keep your sign-in and API keys safe, and tell us quickly if you think someone else has them. - You are responsible for what happens under your workspace, including what agents you connect do with the keys you give them. Agent keys can draft; a person with an owner role approves what goes live. - You must be able to enter a contract, and if you sign up for an organisation, have its authority to do so. ## 3. Your content and your contacts - You keep all rights to the content and contact data you put into Plainsend ("Customer Data"). You give us the rights we need to run the service for you: store it, process it, render and send your messages, and produce your reports. - You are responsible for having a lawful basis to message your contacts, including consent where the law requires it (for Australia, the Spam Act 2003 and the Privacy Act 1988), and for the content you send. - You must follow the [acceptable use policy](https://plainsend.app/legal/acceptable-use.md). - How we handle Customer Data as your processor is set out in the [data processing agreement](https://plainsend.app/legal/dpa.md). ## 4. Sending - Before you can send to your contacts you must verify a sending domain you control. New domains warm up with a daily cap. - We may hold, slow or pause sending to protect deliverability for everyone, for example when bounce or complaint rates cross published thresholds, or when a list looks purchased. We'll tell you why and how to resume. - Solo plan emails carry a small "Sent with Plainsend" line in the footer. ## 5. Plans, payment and cancelling - Prices are on the [pricing page](https://plainsend.app/pricing.md). Solo is billed yearly in advance; other plans as stated when you buy. Prices are in US dollars and exclude taxes, which we add where required. - Payments are processed by Stripe. If a payment fails we'll tell you and may pause sending after [14] days. - You can cancel at any time. Your plan runs to the end of the period you paid for. We don't refund partial periods, except where the law requires it. - We may change prices with at least [30] days' notice; the change applies from your next billing period. ## 6. When it ends You can export your data at any time (contacts, and your workspace as files). After your account closes we keep Customer Data for [30] days so you can still export it, then delete it, except what the law requires us to keep (for example, records of unsubscribes so we never email those people for you again, and invoices). We may suspend or close an account that breaks these terms or the acceptable use policy, or doesn't pay. Where we can, we'll warn you first and give you a chance to fix it. ## 7. Availability and support We aim to keep Plainsend running and to fix problems quickly, but unless you have a signed agreement with a service level, we don't promise a particular uptime. ## 8. Liability - Nothing in these terms excludes rights you have under the Australian Consumer Law that cannot be excluded. - Otherwise, the service is provided "as is", and to the extent the law allows, neither party is liable for indirect or consequential loss, and each party's total liability is limited to the fees you paid in the [12] months before the claim. - You indemnify us against claims arising from the content you send or a breach of your obligations about consent. ## 9. Changes to these terms We may update these terms. For changes that matter, we'll give you [30] days' notice by email or in the console. ## 10. Law These terms are governed by the law of [New South Wales, Australia], and the courts there have jurisdiction. ## Contact [Contact address to be confirmed.] Until then, use the [contact form](https://plainsend.app/switch.md#talk).