Skip to Main Content
Back To Blogs

16th Apr 2026 / 3 min read / Vishnu Sankar

Cloudflare Email Service for agents: What shipped and how to add a safe send guard

Cloudflare Email Service entered public beta on April 16, 2026 with agent-focused tooling. Here is what changed and how to gate sends with an UnwrapEmail API check first.

Cloudflare announced on April 16, 2026 that Email Service is in public beta with a clear focus on agent workflows.

If you build inbox automations, support copilots, or async back-office agents, this release matters because the send and reply loop can stay inside Cloudflare Workers.

What changed in the beta

Based on Cloudflare’s announcement, the practical upgrades are:

  1. Native email sending from Workers so your app can send without bolting on another provider API for the basic flow.
  2. Email MCP + Wrangler tooling so coding agents and developer workflows can configure and trigger email tasks faster.
  3. Async-friendly agent patterns so a Worker can process in the background and reply when work is done.

That combination is especially useful for:

  • support follow-ups after async investigation,
  • invoice or export-ready notifications,
  • approval and human-in-the-loop email steps,
  • long-running agent jobs that need a final threaded reply.

Temp API lib that checks UnwrapEmail before send

Instead of maintaining a temporary regex and disposable-domain list in your Worker, you can call UnwrapEmail first and only send when the address is safe.

type UnwrapEmailValidationResponse = {
  safe_to_send?: boolean
  is_disposable?: boolean
  has_valid_mx_records?: boolean
  reason?: string
}

export type TempEmailPreflightInput = {
  email: string
  apiKey: string
  timeoutMs?: number
}

export type TempEmailPreflightResult =
  | { ok: true }
  | { ok: false; reason: string }

export const runTempEmailPreflight = async (
  input: TempEmailPreflightInput,
): Promise<TempEmailPreflightResult> => {
  const controller = new AbortController()
  const timeout = setTimeout(() => {
    controller.abort()
  }, input.timeoutMs ?? 4_000)

  try {
    const response = await fetch(
      `https://api.unwrap.email/v1/emails/validate?email=${encodeURIComponent(input.email)}&validate_domain=1`,
      {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${input.apiKey}`,
        },
        signal: controller.signal,
      },
    )

    if (!response.ok) {
      return {
        ok: false,
        reason: `UnwrapEmail request failed with status ${response.status}`,
      }
    }

    const validation = (await response.json()) as UnwrapEmailValidationResponse

    if (!validation.safe_to_send) {
      return {
        ok: false,
        reason: validation.reason ?? 'Address is not safe to send.',
      }
    }

    if (validation.is_disposable) {
      return {
        ok: false,
        reason: 'Disposable inboxes are blocked for transactional sends.',
      }
    }

    if (!validation.has_valid_mx_records) {
      return {
        ok: false,
        reason: 'Domain MX records are invalid for delivery.',
      }
    }

    return { ok: true }
  } catch (error) {
    const reason =
      error instanceof Error ? error.message : 'Unknown validation failure.'

    return {
      ok: false,
      reason: `UnwrapEmail request failed: ${reason}`,
    }
  } finally {
    clearTimeout(timeout)
  }
}

Send wrapper for Cloudflare Worker email binding

This wrapper enforces the UnwrapEmail preflight check before any Worker send operation.

import { runTempEmailPreflight } from './temp-email-preflight'

type WorkerEmailBinding = Pick<SendEmail, 'send'>

type SendTransactionalEmailInput = {
  to: string
  from: string
  subject: string
  text: string
  unwrapEmailApiKey: string
}

export const sendTransactionalEmail = async (
  email: WorkerEmailBinding,
  input: SendTransactionalEmailInput,
): Promise<void> => {
  const preflight = await runTempEmailPreflight({
    email: input.to,
    apiKey: input.unwrapEmailApiKey,
  })

  if (!preflight.ok) {
    throw new Error(
      `Email blocked by UnwrapEmail preflight: ${preflight.reason}`,
    )
  }

  await email.send({
    to: input.to,
    from: input.from,
    subject: input.subject,
    text: input.text,
  })
}

Next hardening steps

  • Add retry + circuit-breaker handling around UnwrapEmail fetch failures.
  • Cache preflight results briefly to reduce duplicate checks for repeated sends.
  • Add per-tenant send rate limits and abuse alerting.
  • Log blocked attempts with clear reason codes for auditability.
  • Add unit tests for allowed and blocked preflight outcomes.

Source