Email API

/

JavaScript

Self-Hosted Email API in JavaScript

This guide shows you how to send email from JavaScript using a self-hosted email API. Use plain fetch with zero dependencies — works in Node 18+, Workers, Bun, Deno — or install the official SDK for typed errors and built-in retry policy. Both hit the same wire endpoint running in your Cloudflare account, and the inbox lands in your D1 database.

Install + auth

Plain fetch needs no install — every supported runtime ships it. The SDK adds a typed wrapper with FlowmailsError subclasses and an idempotent retry policy on top. The API key comes from /dashboard/settings/api-keys and is bound to one of your domains.

install.shbash
# Plain fetch (zero dependencies, Node 18+, Workers, Bun, Deno)
# No install step — fetch is built into every supported runtime.

# Official SDK (typed wrapper around the same wire endpoint)
pnpm add @flowmails/flowmails-sdk
# or
npm install @flowmails/flowmails-sdk
# or
yarn add @flowmails/flowmails-sdk

Send with fetch (zero deps)

POST /api/email/send with a JSON body. The from address must land on a domain bound to the API key — the Worker rejects cross-domain sends with a 422 from_domain_mismatch.

send.mjsjavascript
// Plain fetch — works in Node 18+, Workers, Bun, Deno
const API_KEY = process.env.FLOWMAILS_API_KEY; // fm_live_…
const ENDPOINT = "https://your-worker.workers.dev/api/email/send";

const resp = await fetch(ENDPOINT, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "hello@yourdomain.com",
    to: "customer@example.com",
    subject: "Order #1234 confirmed",
    text: "Thanks for your order — we will ship it tomorrow.",
  }),
});

if (!resp.ok) {
  throw new Error(`send failed: ${resp.status} ${await resp.text()}`);
}

console.log(await resp.json());
// { results: [{ id: "msg_8421", status: "queued" }] }

Send with the official SDK

Same wire contract, typed errors. fm.send() auto-retries 5xx / 408 / 429 with exponential backoff, and the five error subclasses (AuthenticationError, ValidationError, RateLimitError, UpstreamError, FlowmailsError) discriminate with instanceof.

send-sdk.mjsjavascript
import { Flowmails } from "@flowmails/flowmails-sdk";

const fm = new Flowmails({
  apiKey: process.env.FLOWMAILS_API_KEY!,
  // baseURL defaults to https://sdk.flowmails.net
  // maxRetries defaults to 2 (5xx / 408 / 429 auto-retried)
});

const result = await fm.send({
  from: "support@yourdomain.com",
  to: ["customer@example.com", "team@yourdomain.com"],
  subject: "Weekly digest",
  html: "<h1>This week</h1><p>Three new signups.</p>",
  attachments: [
    {
      filename: "invoice.pdf",
      content: invoiceBytes,           // Uint8Array or base64 string
      mimeType: "application/pdf",
    },
  ],
});

console.log(result); // { id: "msg_8421", status: "queued" }

Receive inbound webhooks

Cloudflare Email Routing hands every inbound message to your Worker. Forward the JSON-shaped envelope to your service, and ack with a 2xx so Email Routing doesn't retry.

worker.tsts
// Cloudflare Email Routing handler — drop into your Worker
export default {
  async email(message, env, ctx) {
    const payload = {
      messageId: message.headers.get("message-id"),
      from: message.from,
      to: message.to,
      subject: message.headers.get("subject"),
    };
    // Forward to your service's webhook endpoint
    await fetch("https://your-app.example.com/webhooks/inbound", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  },
};

// Receiving side — any HTTPS-capable runtime
export async function handler(req) {
  if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
  const payload = await req.json();
  // … persist, fan out, ack
  return new Response(JSON.stringify({ ok: true }), {
    headers: { "Content-Type": "application/json" },
  });
}

Send your first email from JavaScript in under five minutes.

Start free