Headless CMS for transactional email

Edit the footer once.
Every email updates.

Visual MJML editor with reusable blocks and per-tenant brand variables. Your app triggers emails by posting events — no campaign setup, no contact list to sync.

Not a Mailchimp. Not a Customer.io. Not Postmark alone.

  • No credit card
  • GDPR-ready
  • Hosted in the EU
  • Node.js & Python SDKs

POST an event, our rules decide which templates match, we render and ship through Postmark, webhooks flow back to your endpoint. That's the whole thing.


curl -X POST https://api.else.events/api/events \
  -H "X-API-Key: $ELSE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "9f3c2e1a-8b4d-4f2a-9c1e-5d7a0b3c4e2f",
    "eventName": "user.signed_up",
    "occurredAt": "2026-04-20T12:00:00Z",
    "payload": {
      "user": { "email": "alex@example.com", "firstName": "Alex" },
      "plan": "pro",
      "confirmUrl": "https://app.example.com/confirm/..."
    }
  }'

import { ElseEvents } from "@else.events/sdk";
import { randomUUID } from "node:crypto";

const client = new ElseEvents(process.env.ELSE_API_KEY);

await client.events.send({
  eventId: randomUUID(),
  eventName: "user.signed_up",
  occurredAt: new Date().toISOString(),
  payload: {
    user: { email: user.email, firstName: user.firstName },
    plan: user.plan,
    confirmUrl: buildConfirmUrl(user),
  },
});

from else_events import ElseEvents
from datetime import datetime, timezone
from uuid import uuid4

client = ElseEvents(api_key=os.environ["ELSE_API_KEY"])

client.events.send(
    event_id=str(uuid4()),
    event_name="user.signed_up",
    occurred_at=datetime.now(timezone.utc).isoformat(),
    payload={
        "user": {"email": user.email, "firstName": user.first_name},
        "plan": user.plan,
        "confirmUrl": build_confirm_url(user),
    },
)
Heard on r/webdev, April 2026

Thirty transactional templates. A footer change means updating thirty files. A new CTA, same story. Most builders are designed for campaign flows — compose, schedule, send. But our emails are triggered from the app, not from a marketing calendar. What we actually need is a headless CMS for email.

paraphrased — a developer asking the internet
That's the thing I built

Swap a logo in one block. Every template that references it updates. No find-and-replace across thirty files, no redeploy, no campaign tool in the loop.

Christian, founder
How it works

How an email gets out

You don't need to sync a subscriber list. Your app already knows who to email.

  1. 01

    Send an event

    POST any domain event to /api/events. Names like user.signed_up, order.paid or tournament.started are typical. The JSON payload carries the recipient and whatever your templates need.

    POST /api/events
    {
      "eventId": "9f3c2e1a-...",
      "eventName": "user.signed_up",
      "occurredAt": "2026-04-20T12:00:00Z",
      "payload": {
        "user": { "email": "alex@example.com" },
        "plan": "pro"
      }
    }
  2. 02

    Rules match

    Rules evaluate conditions (equals, contains, gt, regex, JSONPath). Dedupe keys stop duplicate sends, and one event can fan out to several recipients when you need it.

    rule: welcome_pro
    when:
      event.eventName == "user.signed_up"
      AND payload.plan in ["pro","team"]
    audience: payload.user.email
    dedupe: sha256(email) + "pro_welcome"
  3. 03

    Templates render & ship

    MJML templates, reusable blocks, per-tenant brand variables, seasonal variants for things like Black Friday. We render, Postmark delivers, webhooks flow back.

    template: welcome_pro@v3
    └─ header (shared symbol)
    └─ body: {{brand.name}} {{plan}}
    └─ footer (shared symbol)
    → postmark.send → delivered ✓
Why it's different

We don't keep a contact list.

Your users aren't subscribers to us. We send the email via Postmark, then reduce the recipient to a SHA-256 hash in our delivery log. When you open a delivery in the dashboard, the hash is resolved on request. There's no contact table, no audience sync, no PII in the index we query for activity, metrics, or webhooks.

  • Delivery log stores SHA-256 hashes only. No salt, so you can reconcile them yourself.
  • Pay per event, not per user. No contact-based pricing.
  • Your database stays the source of truth for who exists and who opted in.
  • Recipients are only resolved when a human opens a delivery in the dashboard.
Why I'm building this

I needed this for my own stack.

Since 2023 I've been working on a framework to make my client work less repetitive. The first SaaS I built on top of it was turniermeister.com, a tournament platform. The moment I got to the point where it needed to send emails, I ran into a wall.

I asked around. What do people use? Postmark, Mailchimp, Customer.io, the usual suspects. Every single one came with the same two problems: I had to write glue code every time I wanted a new email, and they wanted a copy of my users in their database.

A while back I changed my personal email address. Months later I was still getting newsletters on the old one. Think about that for a second. These are billion-dollar companies whose entire business is managing email lists, and they can't keep their own data in sync. Why would I hand them mine?

So I built the thing I wanted. POST an event, pick a template, swap a logo once and every template updates. Your users stay in your database. All I keep is a hash in a delivery log.

Christian, Carinthia (Austria)

Features

The template system I wish I'd had two years ago.

Visual editor, reusable blocks, variants, locales. Nothing fancy. Just the boring stuff done properly.

  • Visual MJML editor

    Drag-and-drop editor built on GrapesJS and MJML. Renders consistently in Outlook, Gmail and Apple Mail without you hand-writing a single table layout.

  • Reusable blocks

    Header, footer, CTA as symbols. Edit once, every template updates live. Cycle-detected, depth-limited, tenant-scoped.

  • Variants & version history

    Ship seasonal variants (Easter, Black Friday) without forking templates. Full version history with diff and one-click rollback.

  • Multi-locale

    One template, multiple languages. When a translation is missing the fallback chain is deterministic, so you won't ship a blank email by accident.

  • Tenant brand variables

    Logo, primary color, support email, address. Resolved at render time, so whitelabel SaaS works without duplicating templates per tenant.

  • Full observability

    Activity feed, delivery timeline, webhook stats (delivery, open, click, bounce). Dry-run any event before it goes live.

Developer experience

Details you'll notice on day three.

Stable endpoints, signed webhooks, dry-runs from the dashboard. Idempotency via eventId so retries never double-send.

  • Idempotent event ingestion via eventId (UUID)
  • Signed webhook callbacks for delivery / open / click / bounce
  • Dry-run any event from the dashboard
  • Rate limits surfaced in response headers

import { verify } from "@else.events/sdk";

export async function POST(req) {
  const body = await req.text();
  const signature = req.headers.get("x-else-signature");

  if (!verify(body, signature, process.env.ELSE_WEBHOOK_SECRET)) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);
  //  event.type: "delivery" | "open" | "click" | "bounce" | "complaint"
  //  event.recipientHash — match against your users
  //  event.deliveryId   — correlate with what you sent

  await mirrorToAnalytics(event);
  return new Response("ok");
}

curl -X POST https://api.else.events/v1/dry-run \
  -H "Authorization: Bearer $ELSE_API_KEY" \
  -d '{
    "ruleId": "welcome_pro",
    "payload": { "plan": "pro", "firstName": "Alex" }
  }'

# returns: rendered HTML + the Postmark payload
# that WOULD be sent — without actually sending.
Pricing

Three plans. Pay per event, not per contact.

Public Beta pricing. Your account keeps this price for as long as it stays active.

Save ~20% with yearly billing.

Not sure which plan fits?

Estimate your volume — we'll pick the cheapest plan, overage fees included.

Estimated volume ~20,000 emails / month
Cheapest plan for you Basic
Your total cost ≈ €13 / month
  • Premium

    For when log retention and throughput start to matter.

    €129 €103 / month
    billed annually Post-launch rate: €149/mo — yours is locked in. Start on Premium
    • 500,000 events · 100,000 emails / month
    • +€1.50 per 1,000 extra emails
    • 1-year log retention · analytics mirror
    • Priority Slack support with SLA
  • Recommended

    Pro

    Ready for production. Metered emails, predictable bill.

    €39 €31 / month
    billed annually Start on Pro
    • 50,000 events · 25,000 emails / month
    • +€2 per 1,000 extra emails
    • 50 templates · 5 variants · 5 locales
    • Email support within 48h
  • Basic

    For side projects and early validation.

    €7 €5 / month
    billed annually Start on Basic
    • 1,000 events · 3,000 emails / month
    • +€3 per 1,000 extra emails
    • 5 templates · 1 locale · 1 seat
    • Community support

14-day free trial on sign-up No auto-charge after trial Cancel anytime Beta prices locked in

Compare all features

Emails are the metered resource — each event can trigger one or more emails (transactional plus any templates matched by your rules). BYOM (bring your own mail provider) is on the roadmap; current plans send via shared Postmark infrastructure.

Feature Premium Pro Basic
Events / month 500,000 50,000 1,000
Emails included / month 100,000 25,000 3,000
Email overage +€1.50 / 1k +€2 / 1k +€3 / 1k
Rate limit (/api/events) 300 / min 60 / min 10 / min
Templates (total / active) 50 / 25 5 / 3
Variants per template 5 1
Version history 100 10
Locales per template 5 1
Log retention 1 year 30 days 7 days
Analytics mirror
Bring your own mail (planned)
Support Slack / Priority Email <48h Community
Workspace seats 10 1

Need custom limits or a DPA? Talk to us →

FAQ

Questions people keep asking

Do you store our users' email addresses?

Not as a contact list. The delivery log (the table we query for activity, metrics and webhook routing) stores only a SHA-256 hash of the recipient. It's unsalted, so you can compute it yourself. The plaintext lives inside the event you posted, used once at send-time by Postmark. When you open a delivery in the dashboard, we re-resolve the address on request. Nothing is cached or aggregated.

Why not use Postmark directly?

You can. else.events adds the layer most teams eventually build on top of Postmark: a visual MJML editor with reusable blocks, template inheritance, seasonal variants, version history, multi-tenancy, rule engine, activity feed, and webhook observability.

What are the rate limits?

Basic is capped at 10 requests/min on /api/events, Pro at 60/min, Premium at 300/min. All limits are visible as response headers.

Which email clients are supported?

Templates are MJML-based, so they render consistently in Outlook (including legacy), Gmail, Apple Mail, and major mobile clients.

Do you provide SDKs?

Node.js, Python, and a raw OpenAPI spec for generated SDKs. More languages based on demand.

What happens on bounces and complaints?

Every webhook from Postmark (delivery, open, click, bounce, complaint) is forwarded to your endpoint, and the same events show up in the dashboard. Suppression lives on your side, because your consent state lives on your side.

Is it GDPR-compliant?

By design. Because we don't store PII for recipient identity, the GDPR obligations shift: your data processor agreement with us becomes trivially narrow. Full DPA available on request.

Start sending events.

Create a workspace, grab an API key, POST your first event. If you've worked with a transactional email API before, this is a couple of hours of work, not a sprint.