Skip to content

Integrating notify-flow into a web app ​

This guide shows how a web app's backend sends transactional emails through notify-flow. To set up and deploy the service itself, see Getting started. Part 2 covers flows, templates, retries and production.

Overview ​

To send a transactional email, your web app's backend makes one HTTP call to notify-flow, with a template name (or an event) and its variables. notify-flow does the rest and returns a delivery id.

notify-flow handlesYour web app handles
Authenticating your app with its API keyDeciding when to send (sign-up, password reset, login…)
Picking the template and locale, rendering subject, HTML and textGenerating tokens and links (verification, reset) and checking them
Sending over SMTP (Gmail, Microsoft 365 or generic SMTP), with retriesKeeping the API key in the backend only
Rate limiting and idempotencyHandling the result of the call (errors, retries)
Logging the delivery outcomeShowing the user a neutral message

notify-flow only delivers email: it doesn't know your users and doesn't handle passwords or tokens.

Prerequisites ​

The notify-flow administrator gives each web app a URL and its own API key. Both go in the web app's backend, never in the frontend.

WhatValueWhere it goes
Service URLhttps://notify-flow.<your-subdomain>.workers.devBackend environment variable, e.g. NOTIFY_FLOW_URL
API keynf_…, shown once when it is createdBackend secret, e.g. NOTIFY_FLOW_API_KEY
App nameFree text, e.g. AcmeSent with every email as the appName variable

The administrator creates the key with npm run client:create -- <app-name> --remote. Use one key per web app: you can revoke one without touching the others, and each app gets its own templates and limits.

If the key ends up in the frontend, in a repository or in a chat, treat it as compromised: ask the administrator to create a new one and delete the old one.

Architecture ​

The frontend never talks to notify-flow. It asks your app's backend for an action, and the backend calls notify-flow with the API key.

text
┌──────────────────────────────────────────────────────────┐
│ Frontend (browser or mobile app)                         │
│ Asks for an action: sign-up, password reset, login.      │
│ Doesn't know the API key and never calls notify-flow.    │
└──────────────────────────────────────────────────────────┘
                             │  POST /forgot-password
                             ▼  (no API key)
┌──────────────────────────────────────────────────────────┐
│ Your web app's backend               ◄── you build this  │
│ Checks the user, generates tokens and links.             │
│ Holds the API key and calls notify-flow.                 │
└──────────────────────────────────────────────────────────┘
                             │  POST /v1/email/send with key and Idempotency-Key
                             ▼  response: id and status
┌──────────────────────────────────────────────────────────┐
│ notify-flow (Cloudflare Worker)                          │
│ Authenticates, validates, applies limits and idempotency.│
│ Renders the template, sends with retries, logs to D1.    │
└──────────────────────────────────────────────────────────┘
                             │
                             ▼  SMTP over TLS (465) or STARTTLS (587)
┌──────────────────────────────────────────────────────────┐
│ SMTP server (Gmail, Microsoft 365 or other)              │
│ Accepts the message: the delivery becomes submitted.     │
└──────────────────────────────────────────────────────────┘
                             │
                             ▼  delivery
┌──────────────────────────────────────────────────────────┐
│ The user's inbox                                         │
└──────────────────────────────────────────────────────────┘

The backend box is the part you build. Everything below it is handled by notify-flow.

Sending an email ​

Every send is a POST /v1/email/send. On success it returns 200 with {"id":"msg_…","status":"submitted"}.

bash
curl -X POST "$NOTIFY_FLOW_URL/v1/email/send" \
  -H "Authorization: Bearer $NOTIFY_FLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: password-reset:user-123:req-456" \
  -d '{
    "template": "password-reset",
    "to": "jane.doe@example.com",
    "locale": "en",
    "variables": {
      "appName": "Acme",
      "name": "Jane",
      "resetUrl": "https://app.acme.com/reset?token=..."
    }
  }'

submitted means the SMTP server accepted the message, not that it reached the recipient's inbox.

Field rules and every error code: API Reference. What to retry: Errors, retries and idempotency.

For asynchronous sending (202 Accepted, GET /v1/email/:id), see Asynchronous sending. To send by event instead of by template name (POST /v1/notifications), see Event-based sending.

Backend code examples ​

All you need is one function that makes the POST and turns errors into exceptions. No library required: fetch in Node 18+, requests in Python.

Node.js / TypeScript:

typescript
const NOTIFY_URL = process.env.NOTIFY_FLOW_URL!;      // https://notify-flow.<your-subdomain>.workers.dev
const NOTIFY_KEY = process.env.NOTIFY_FLOW_API_KEY!;  // nf_...

export async function sendEmail(opts: {
  template: string;
  to: string;
  variables: Record<string, string>;
  locale?: "it" | "en" | "pt-BR";
  idempotencyKey?: string;
}): Promise<{ id: string; status: string }> {
  const res = await fetch(`${NOTIFY_URL}/v1/email/send`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${NOTIFY_KEY}`,
      "Content-Type": "application/json",
      ...(opts.idempotencyKey && { "Idempotency-Key": opts.idempotencyKey }),
    },
    body: JSON.stringify({
      template: opts.template,
      to: opts.to,
      locale: opts.locale ?? "en",
      variables: { appName: "Acme", ...opts.variables },
    }),
    signal: AbortSignal.timeout(30_000),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`notify-flow ${res.status} ${body.error?.code}`);
  return body;
}

Python:

python
import os, requests

NOTIFY_URL = os.environ["NOTIFY_FLOW_URL"]
NOTIFY_KEY = os.environ["NOTIFY_FLOW_API_KEY"]

def send_email(template, to, variables, locale="en", idempotency_key=None):
    headers = {"Authorization": f"Bearer {NOTIFY_KEY}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    res = requests.post(
        f"{NOTIFY_URL}/v1/email/send",
        json={"template": template, "to": to, "locale": locale,
              "variables": {"appName": "Acme", **variables}},
        headers=headers,
        timeout=30,
    )
    body = res.json()
    if not res.ok:
        raise RuntimeError(f"notify-flow {res.status_code} {body['error']['code']}")
    return body

The 30-second timeout is there because notify-flow retries transient SMTP errors before answering, so a slow response is normal.

Next ​

Continue with flows, custom templates, retries, security and the production checklist.