Event-driven email infrastructure for SaaS

Product emails from events.
Templates managed like a CMS.

Your SaaS posts events like user.signed_up, payment.failed or team.member_invited. else.events matches rules, renders MJML templates and ships through Postmark or SMTP — without campaign setup or contact list sync.

Not a newsletter platform. Not a marketing suite. Not Postmark alone.

  • No credit card
  • GDPR-ready
  • Hosted in the EU
  • AI drafts · bring your own key

POST an event, our rules pick the matching template (drafted with AI, polished in the visual editor), we render and ship through your provider (Postmark or SMTP), webhooks flow back. That's the whole thing.


curl -X POST https://app.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 { randomUUID } from "node:crypto";

await fetch("https://app.else.events/api/events", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.ELSE_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    eventId: randomUUID(),
    eventName: "user.signed_up",
    occurredAt: new Date().toISOString(),
    payload: {
      user: { email: user.email, firstName: user.firstName },
      plan: user.plan,
      confirmUrl: buildConfirmUrl(user),
    },
  }),
});

import os, requests
from datetime import datetime, timezone
from uuid import uuid4

requests.post(
    "https://app.else.events/api/events",
    headers={"X-API-Key": os.environ["ELSE_API_KEY"]},
    json={
        "eventId": str(uuid4()),
        "eventName": "user.signed_up",
        "occurredAt": datetime.now(timezone.utc).isoformat(),
        "payload": {
            "user": {"email": user.email, "firstName": user.first_name},
            "plan": user.plan,
            "confirmUrl": build_confirm_url(user),
        },
    },
    timeout=10,
)
Heard on r/webdev, April 2026

Our transactional emails fire on app events — welcome on sign-up, dunning on payment failed, invite on team change. But every tool I look at is campaign-first: schedule, segment, send. Meanwhile thirty MJML templates rot in /emails, every footer change is thirty PRs. What I actually need is events in, templates out, with a CMS in between.

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

Your app posts an event. A rule picks the template. The renderer assembles the blocks — header, footer, CTA — and ships through your provider. Swap a logo in one block, every template updates. No find-and-replace, no redeploy, no campaign tool in the loop.

Christian, founder

For product emails your SaaS sends automatically.

Built around product events, rules and transactional templates.

  • Welcome and email verification
  • Trial onboarding and activation nudges
  • Payment failed and renewal receipts
  • Team invitations and permission changes
  • Usage limits and upgrade prompts
  • Product notifications and digests
  • Tenant-branded customer emails

Not built for newsletter businesses.

We don't try to replace creator newsletter platforms, sponsor marketplaces or campaign-heavy marketing suites.

  • Creator newsletters
  • Sponsor marketplaces
  • List monetization
  • One-off mass campaigns
  • Audience segmentation tools
  • Campaign calendars

Newsletter tools manage audiences. else.events processes product state — an event happens, a rule matches, a template renders, an email is sent.

How an email gets out

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

  1. 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. Rules match

    Rules evaluate conditions (equals, gt/lt, in, exists) over the JSON payload using dot-paths. 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. Templates render & ship

    MJML templates, reusable blocks, per-tenant brand variables, seasonal variants for things like Black Friday. We render, your email provider (Postmark by default, or SMTP) delivers, webhooks flow back.

    template: welcome_pro@v3
    └─ header (shared symbol)
    └─ body: {{brand.name}} {{plan}}
    └─ footer (shared symbol)
    → provider.send → delivered ✓

We don't keep a contact list.

Your users aren't subscribers to us. We hand the email to your configured email provider (Postmark by default, or SMTP) and 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.

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)

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 editor, MJML output

    Outline tree on the left, live iframe preview on the right, property pane on the right edge. Compiles to MJML on save, so the same template renders consistently in Outlook, Gmail and Apple Mail without you hand-writing a single table layout.

  • AI-generated drafts

    Describe the email in plain language and the editor returns a typed draft — sections, blocks, brand variables already wired up. Then you refine in the visual editor. Bring your own API key, so prompts and tokens stay on your account.

// Live inheritance demo

Pick a brand. Every email updates.

One block, three templates, three tenants. The header block reads the active tenant variables — change the tenant and the same template renders in their colours, with their logo, no fork required.

  • welcome.email
  • invoice.email
  • receipt.email
  • Reusable building blocks

    Header, footer, CTA as first-class building blocks. Edit once, every template that references them 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.

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
  • 429 with Retry-After when rate limits are hit

import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(req) {
  const rawBody = await req.text();
  const sigHeader = req.headers.get("x-else-signature") ?? "";
  const ts = req.headers.get("x-else-timestamp") ?? "";

  const expected = createHmac("sha256", process.env.ELSE_WEBHOOK_SECRET)
    .update(`${ts}.${rawBody}`)
    .digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(sigHeader.replace(/^sha256=/, ""));
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(rawBody);
  //  event.type: "email.delivered" | "email.bounced" | "email.opened" | ...
  //  event.data.recipientHash — match against your users
  //  event.data.deliveryId    — correlate with what you sent

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

curl -X POST https://app.else.events/api/events \
  -H "X-API-Key: $ELSE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventName": "user.welcome",
    "payload": {
      "recipientEmail": "alex@example.com",
      "firstName": "Alex",
      "plan": "pro"
    }
  }'

# else.events matches a rule, renders the template
# and dispatches via your provider — you get a
# delivery id back and webhook events as it progresses.

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.

    €149 €119 / month
    billed annually Start on Premium
    • 500,000 events · 100,000 emails / month
    • +€1.50 per 1,000 extra emails
    • 1-year log retention
    • 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). By default we deliver through shared Postmark infrastructure; SMTP is already supported for bring-your-own setups, additional providers are on the roadmap.

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
Bring your own SMTP
Support Slack / Priority Email <48h Community
Workspace seats 10 1

Need custom limits or a DPA?   Talk to us →

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 your configured email provider (Postmark by default, SMTP optional). When you open a delivery in the dashboard, we re-resolve the address on request. Nothing is cached or aggregated.

Why not use Postmark (or SMTP) directly?

You can. else.events adds the layer most teams eventually build on top of an email provider: a visual MJML editor with reusable blocks, template inheritance, seasonal variants, version history, multi-tenancy, rule engine, activity feed, and webhook observability — regardless of whether you ship via Postmark, your own SMTP relay, or another provider down the line.

What are the rate limits?

During the public beta, /api/events carries a 10 requests/min/IP anti-abuse floor (the rest of the API uses the global 100 requests/min/IP default). Tier-specific limits ship with the GA pricing rollout. Hitting either cap returns 429 with a Retry-After header.

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?

The API is one POST per event plus signed webhooks, so we ship copy-paste examples in Node.js, Python and curl rather than versioned SDK packages. An OpenAPI spec for auto-generated clients is on the roadmap.

What happens on bounces and complaints?

Every webhook from your email provider (delivery, open, click, bounce, complaint) is normalised and 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.