All skills
cloudflare avatar

/cloudflare-email-service

@41e0d19 official
by cloudflarecloudflare/skills3k stars
298

Implement or troubleshoot Cloudflare Email Sending and Email Routing integrations and their delivery configuration.

Use this Skill: https://skilld.dev/gh/cloudflare/skills/cloudflare-email-service

This session only. Nothing lands on disk.

referencesdeliverability.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Email Deliverability & Best Practices

For full details, see the deliverability docs and email authentication docs. All the monitoring endpoints below can be called via the REST API, Wrangler CLI, or the Cloudflare MCP server.

What Cloudflare Handles

When you onboard a domain, Cloudflare auto-configures:

  • SPF — TXT records authorizing Cloudflare's sending infrastructure
  • DKIM — Records for cryptographic signing of outbound emails
  • IP reputation — Managed sending infrastructure optimized for deliverability
  • Soft bounce retries — Automatic exponential backoff for temporary failures
  • Suppression lists — Hard-bounced addresses automatically blocked
  • Feedback loops — ISP complaint signals processed and acted on

Consider adding a DMARC record if you don't have one: v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com

Bounce Handling

Hard bounces — permanent failures (address doesn't exist, domain doesn't exist). Never retried. Address auto-added to suppression list. Sending to suppressed address returns E_RECIPIENT_SUPPRESSED.

Soft bounces — temporary failures (mailbox full, server down, greylisting). Cloudflare auto-retries with exponential backoff.

Suppression Lists

Account list (your account) — spam complaints from recipients. Cloudflare integrates with Postmasters to auto-suppress. You can manually add/remove addresses in the Dashboard.

See the suppressions docs for details.

Your Responsibilities

Content

  • Include both HTML and plain text versions
  • Use a recognizable sender name: { email: "noreply@app.com", name: "My App" }
  • Write honest subject lines — avoid ALL CAPS, excessive punctuation
  • Include List-Unsubscribe headers for recurring emails
  • Use full URLs from your domain — avoid URL shorteners

List Quality

  • Validate email addresses before sending
  • Implement double opt-in for subscriptions
  • Honor unsubscribe requests promptly

Transactional Only

Email Service is for transactional email (triggered by user actions: signups, password resets, order confirmations). Marketing/bulk campaigns are not permitted — use a dedicated marketing platform.

Monitoring Deliverability

Dashboard

Per-domain and account-wide analytics are available in the Cloudflare dashboard:

  1. Log in to the Cloudflare dashboard and select your account.
  2. Go to Compute & AI > Email Service.
  3. Select a domain or view account-wide metrics.
  4. Select the Analytics tab.

Send Response

Every send (REST API or Workers binding) returns immediate delivery feedback. Check the response to track per-send outcomes:

{
  "result": {
    "delivered": ["user@example.com"],
    "permanent_bounces": ["bad@nonexistent.com"],
    "queued": ["slow@recipient.com"]
  }
}

Log these to build your own delivery rate metrics.

Sending Limits

Check your account's daily sending quota:

curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/limits" \
  --header "Authorization: Bearer <API_TOKEN>"

Returns:

{
  "result": {
    "quota": { "value": 5000, "unit": "day" }
  }
}

Suppression List

Addresses that hard-bounced or received spam complaints are auto-suppressed. You can query and manage suppressions via the API.

List suppressions (account-wide):

curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression?page=1&per_page=100&order=created_at&direction=desc" \
  --header "Authorization: Bearer <API_TOKEN>"

Returns:

{
  "page": 1,
  "per_page": 100,
  "total": 2,
  "result": [
    {
      "id": "396a5436-d4b0-42a6-b3fc-48e8fa522321",
      "email": "bounced@example.com",
      "reason": "hard_bounce",
      "created_at": "2026-03-15T10:00:00Z",
      "expires_at": null
    }
  ]
}

Query params: page, per_page (max 1000), order (email | expires_at | created_at), direction (asc | desc).

Manually suppress an address:

curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression" \
  --header "Authorization: Bearer <API_TOKEN>" \
  --header "Content-Type: application/json" \
  --data '{ "email": "user@example.com", "expires_at": "2026-06-01T00:00:00Z" }'

expires_at is optional — omit for permanent suppression.

Remove a suppression:

curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression/{suppression_id}" \
  --header "Authorization: Bearer <API_TOKEN>"

Zone-level suppressions are also available at /zones/{zone_id}/email/sending/suppression with the same interface.

GraphQL Analytics API

Email Service exposes two zone-level datasets via the GraphQL Analytics API. You can explore the schema interactively at graphql.cloudflare.com/explorer. Metrics are retained for 31 days.

Dataset Description
emailSendingAdaptiveGroups Aggregated counts grouped by dimensions (status, date, domain, auth results, etc.)
emailSendingAdaptive Individual email events with full detail (from, to, subject, messageId, errors, etc.)

These are zone-level datasets — query under viewer > zones, not accounts.

Aggregated dimensions (emailSendingAdaptiveGroups):

Dimension Type Description
date Date Day-level grouping
datetime Time Exact timestamp (also: datetimeMinute, datetimeFiveMinutes, datetimeFifteenMinutes, datetimeHour)
status string Delivery status
eventType string Event type
sendingDomain string The sending domain
envelopeTo string Recipient address
errorCause string Error cause for failed sends
arc, dkim, dmarc, spf string Email authentication results
isSpam, isNDR, isLastEvent uint8 Boolean flags
spamScore, spamThreshold uint32 Spam scoring

Individual event fields (emailSendingAdaptive) additionally include: from, to, subject, messageId, sessionId, errorDetail.

Email counts by status and date:

query EmailSendingByStatus(
  $zoneTag: string!
  $start: Date!
  $end: Date!
) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      emailSendingAdaptiveGroups(
        filter: { date_geq: $start, date_leq: $end }
        limit: 10000
        orderBy: [date_DESC]
      ) {
        count
        dimensions {
          date
          status
        }
      }
    }
  }
}

Filter by status (e.g. only failures):

query EmailFailures(
  $zoneTag: string!
  $start: Date!
  $end: Date!
) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      emailSendingAdaptiveGroups(
        filter: { date_geq: $start, date_leq: $end, status: "deliveryFailed" }
        limit: 10000
        orderBy: [date_DESC]
      ) {
        count
        dimensions {
          date
          errorCause
          sendingDomain
        }
      }
    }
  }
}

Individual email events (troubleshooting):

query RecentEmailEvents(
  $zoneTag: string!
  $start: Time!
  $end: Time!
) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      emailSendingAdaptive(
        filter: { datetime_geq: $start, datetime_leq: $end }
        limit: 50
        orderBy: [datetime_DESC]
      ) {
        datetime
        from
        to
        subject
        status
        eventType
        sendingDomain
        messageId
        errorCause
        errorDetail
        dkim
        dmarc
        spf
        isSpam
      }
    }
  }
}

Note: emailSendingAdaptive filters use datetime_geq/datetime_leq (Time type, e.g. "2026-04-01T00:00:00Z"), while emailSendingAdaptiveGroups uses date_geq/date_leq (Date type, e.g. "2026-04-01").

curl example:

curl "https://api.cloudflare.com/client/v4/graphql" \
  --header "Authorization: Bearer <API_TOKEN>" \
  --header "Content-Type: application/json" \
  --data '{
    "query": "query($zoneTag:string!,$start:Date!,$end:Date!){viewer{zones(filter:{zoneTag:$zoneTag}){emailSendingAdaptiveGroups(filter:{date_geq:$start,date_leq:$end},limit:10000,orderBy:[date_DESC]){count,dimensions{date,status}}}}}",
    "variables": {
      "zoneTag": "<ZONE_ID>",
      "start": "2026-03-15",
      "end": "2026-04-15"
    }
  }'

Metrics to Watch

Metric Target If Out of Range
Delivery rate > 95% Check for invalid addresses; verify DNS records
Hard bounce rate < 2% Clean your email list
Complaint rate < 0.1% Make unsubscribe easier; stop unwanted emails

Source: SKILL.md on GitHub

1 warning16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides documentation and implementation guides for the Cloudflare Email Service. It includes security considerations regarding the handling of untrusted email data and the use of external development tools, all of which are consistent with the skill's intended purpose.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 41e0d19. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 10 hours ago.

Activeupdated 11 hours ago
  • Python
  • cloudflare
  • email-sending
  • email-routing
  • workers
  • transactional-email
  • rest-api
  • nodejs
  • golang

README badge

README badge for cloudflare/skills/cloudflare-email-service

Send transactional emails and route incoming messages using Cloudflare Email Service, with Workers bindings, REST API, or the Agents SDK. Use this skill for email setup, authentication (SPF/DKIM/DMARC), deliverability tuning, and integrating email into Workers, Node.js, Python, Go, or any AI coding agent.

Generated from the current SKILL.md.

Does this skill cover both sending and receiving emails?
Yes. It covers Email Sending (transactional emails via Workers binding or REST API) and Email Routing (inbound email handling with the `email()` handler). The references are split by use case.
Can I use this skill outside of Cloudflare Workers?
Yes. The REST API works from Node.js, Python, Go, or any external app. You'll need a Cloudflare API token and account ID instead of a binding.
Do I need to set up my domain before sending?
Yes. The domain must be onboarded to Email Sending first via `wrangler email sending enable yourdomain.com` or the Cloudflare Dashboard. The `from` address must use an onboarded domain.
What's the difference between the Workers binding and the REST API?
The Workers binding requires no API keys and uses camelCase field names (`email`, `replyTo`). The REST API uses snake_case (`address`, `reply_to`) and requires a Bearer token, but works outside Workers.
Can I use this for marketing emails or newsletters?
No. Email Service is for transactional emails only. Use a dedicated marketing platform for bulk sends and campaigns.

Generated from the current SKILL.md. These answers refresh after source changes.