---
title: Email
description: Send and receive email from Cloudflare Agents using the Email Service binding and inbound routing rules.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt  
> Use this file to discover all available pages before exploring further.

[Skip to content](#%5Ftop) 

# Email

Agents can send and receive email with Cloudflare [Email Service](https://developers.cloudflare.com/email-routing/email-workers/). This guide shows how to send outbound email with the Workers binding, route inbound mail into Agents, and handle follow-up replies securely.

## Prerequisites

Before using email with Agents, you need:

1. A domain onboarded to [Cloudflare Email Service](https://developers.cloudflare.com/email-routing/).
2. A `send_email` binding in `wrangler.jsonc` for outbound email.
3. An Email Service routing rule that sends inbound mail to your Worker.
4. Optional: an `EMAIL_SECRET` secret if you want secure reply routing.

### Domain setup

1. Log in to the [Cloudflare Dashboard ↗](https://dash.cloudflare.com).
2. Go to **Compute & AI** \> **Email Service**.
3. Select **Onboard Domain** and choose your domain.
4. Add the DNS records (SPF and DKIM) to authorize sending.

DNS changes usually complete within 5-15 minutes for domains using Cloudflare DNS, but can take up to 24 hours to propagate globally.

### Wrangler configuration

Add the email binding to your Worker:

* [  wrangler.jsonc ](#tab-panel-3392)
* [  wrangler.toml ](#tab-panel-3393)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "send_email": [

    {

      "name": "EMAIL",

      "remote": true

    }

  ]

}


```

TOML

```

[[send_email]]

name = "EMAIL"

remote = true


```

The `remote = true` option lets you call the real Email Service API during local development with `wrangler dev`.

## Quick start

* [  JavaScript ](#tab-panel-3418)
* [  TypeScript ](#tab-panel-3419)

JavaScript

```

import { Agent, callable, routeAgentEmail } from "agents";

import { createAddressBasedEmailResolver } from "agents/email";

import PostalMime from "postal-mime";


export class EmailAgent extends Agent {

  @callable()

  async sendWelcomeEmail(to) {

    await this.sendEmail({

      binding: this.env.EMAIL,

      to,

      from: "support@yourdomain.com",

      replyTo: "support@yourdomain.com",

      subject: "Welcome to our service",

      text: "Thanks for signing up. Reply to this email if you need help.",

    });

  }


  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log("Received email from:", email.from);

    console.log("Subject:", parsed.subject);


    await this.replyToEmail(email, {

      fromName: "Support Agent",

      body: "Thanks for your email! We received it.",

    });

  }

}


export default {

  async email(message, env) {

    await routeAgentEmail(message, env, {

      resolver: createAddressBasedEmailResolver("EmailAgent"),

    });

  },

};


```

TypeScript

```

import { Agent, callable, routeAgentEmail } from "agents";

import { createAddressBasedEmailResolver, type AgentEmail } from "agents/email";

import PostalMime from "postal-mime";


export class EmailAgent extends Agent {

  @callable()

  async sendWelcomeEmail(to: string) {

    await this.sendEmail({

      binding: this.env.EMAIL,

      to,

      from: "support@yourdomain.com",

      replyTo: "support@yourdomain.com",

      subject: "Welcome to our service",

      text: "Thanks for signing up. Reply to this email if you need help.",

    });

  }


  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log("Received email from:", email.from);

    console.log("Subject:", parsed.subject);


    await this.replyToEmail(email, {

      fromName: "Support Agent",

      body: "Thanks for your email! We received it.",

    });

  }

}


export default {

  async email(message, env) {

    await routeAgentEmail(message, env, {

      resolver: createAddressBasedEmailResolver("EmailAgent"),

    });

  },

} satisfies ExportedHandler<Env>;


```

## Sending outbound email

### Using `sendEmail()`

`sendEmail()` sends outbound email through a `send_email` binding that you pass explicitly. It automatically injects agent routing headers (`X-Agent-Name`, `X-Agent-ID`) into every message, and optionally signs them with HMAC-SHA256 so that replies can be routed back to the same agent instance.

* [  JavaScript ](#tab-panel-3402)
* [  TypeScript ](#tab-panel-3403)

JavaScript

```

class MyAgent extends Agent {

  @callable()

  async sendReceipt(to, orderId) {

    const result = await this.sendEmail({

      binding: this.env.EMAIL,

      to,

      from: { email: "billing@yourdomain.com", name: "Billing Bot" },

      replyTo: "billing@yourdomain.com",

      subject: `Receipt for order ${orderId}`,

      text: `Your receipt for order ${orderId} is ready.`,

      secret: this.env.EMAIL_SECRET,

    });


    return result.messageId;

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  @callable()

  async sendReceipt(to: string, orderId: string) {

    const result = await this.sendEmail({

      binding: this.env.EMAIL,

      to,

      from: { email: "billing@yourdomain.com", name: "Billing Bot" },

      replyTo: "billing@yourdomain.com",

      subject: `Receipt for order ${orderId}`,

      text: `Your receipt for order ${orderId} is ready.`,

      secret: this.env.EMAIL_SECRET,

    });


    return result.messageId;

  }

}


```

When `secret` is provided, the agent signs the routing headers so that replies verified by `createSecureReplyEmailResolver` route back to the same agent instance.

Set `replyTo` to the mailbox that routes back to your Worker when you want recipients to continue the conversation with the same agent.

## Routing inbound mail

Resolvers determine which Agent instance receives an incoming email. Choose the resolver that matches your use case.

For basic Email Service sending and receiving, `createAddressBasedEmailResolver()` is enough. The secure reply resolver below is optional and specific to Agents SDK reply signing, not a requirement of Email Service itself.

### `createAddressBasedEmailResolver`

Recommended for inbound mail. Routes emails based on the recipient address.

* [  JavaScript ](#tab-panel-3396)
* [  TypeScript ](#tab-panel-3397)

JavaScript

```

import { createAddressBasedEmailResolver } from "agents/email";


const resolver = createAddressBasedEmailResolver("EmailAgent");


```

TypeScript

```

import { createAddressBasedEmailResolver } from "agents/email";


const resolver = createAddressBasedEmailResolver("EmailAgent");


```

**Routing logic:**

| Recipient Address                     | Agent Name           | Agent ID |
| ------------------------------------- | -------------------- | -------- |
| support@example.com                   | EmailAgent (default) | support  |
| sales@example.com                     | EmailAgent (default) | sales    |
| NotificationAgent+user123@example.com | NotificationAgent    | user123  |

The sub-address format (`agent+id@domain`) allows routing to different agent namespaces and instances from a single email domain.

Note

Agent class names in the recipient address are matched case-insensitively. Email infrastructure often lowercases addresses, so `NotificationAgent+user123@example.com` and `notificationagent+user123@example.com` both route to the `NotificationAgent` class.

### `createSecureReplyEmailResolver`

For reply flows with signature verification. Verifies that incoming emails are authentic replies to your outbound emails, preventing attackers from routing emails to arbitrary agent instances.

* [  JavaScript ](#tab-panel-3398)
* [  TypeScript ](#tab-panel-3399)

JavaScript

```

import { createSecureReplyEmailResolver } from "agents/email";


const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);


```

TypeScript

```

import { createSecureReplyEmailResolver } from "agents/email";


const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);


```

When your agent sends an email with `replyToEmail()` or `sendEmail()` and a `secret`, it signs the routing headers with a timestamp. When a reply comes back, this resolver verifies the signature and checks that it has not expired before routing.

**Options:**

* [  JavaScript ](#tab-panel-3404)
* [  TypeScript ](#tab-panel-3405)

JavaScript

```

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {

  // Maximum age of signature in seconds (default: 30 days)

  maxAge: 7 * 24 * 60 * 60, // 7 days


  // Callback for logging/debugging signature failures

  onInvalidSignature: (email, reason) => {

    console.warn(`Invalid signature from ${email.from}: ${reason}`);

    // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"

  },

});


```

TypeScript

```

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {

  // Maximum age of signature in seconds (default: 30 days)

  maxAge: 7 * 24 * 60 * 60, // 7 days


  // Callback for logging/debugging signature failures

  onInvalidSignature: (email, reason) => {

    console.warn(`Invalid signature from ${email.from}: ${reason}`);

    // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"

  },

});


```

**When to use:** If your agent initiates email conversations and you need replies to route back to the same agent instance securely.

### `createCatchAllEmailResolver`

For single-instance routing. Routes all emails to a specific agent instance regardless of the recipient address.

* [  JavaScript ](#tab-panel-3400)
* [  TypeScript ](#tab-panel-3401)

JavaScript

```

import { createCatchAllEmailResolver } from "agents/email";


const resolver = createCatchAllEmailResolver("EmailAgent", "default");


```

TypeScript

```

import { createCatchAllEmailResolver } from "agents/email";


const resolver = createCatchAllEmailResolver("EmailAgent", "default");


```

**When to use:** When you have a single agent instance that handles all emails (for example, a shared inbox).

### Combining resolvers

You can combine resolvers to handle different scenarios:

* [  JavaScript ](#tab-panel-3416)
* [  TypeScript ](#tab-panel-3417)

JavaScript

```

export default {

  async email(message, env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        // First, check if this is a signed reply

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;


        // Otherwise, route based on recipient address

        return addressResolver(email, env);

      },


      // Handle emails that do not match any routing rule

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

};


```

TypeScript

```

export default {

  async email(message, env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        // First, check if this is a signed reply

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;


        // Otherwise, route based on recipient address

        return addressResolver(email, env);

      },


      // Handle emails that do not match any routing rule

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

} satisfies ExportedHandler<Env>;


```

## Handling emails in your Agent

### The `AgentEmail` interface

When your agent's `onEmail` method is called, it receives an `AgentEmail` object:

TypeScript

```

type AgentEmail = {

  from: string; // Sender's email address

  to: string; // Recipient's email address

  headers: Headers; // Email headers (subject, message-id, etc.)

  rawSize: number; // Size of the raw email in bytes


  getRaw(): Promise<Uint8Array>; // Get the full raw email content

  reply(options): Promise<void>; // Send a reply

  forward(rcptTo, headers?): Promise<void>; // Forward the email

  setReject(reason): void; // Reject the email with a reason

};


```

### Parsing email content

Use a library like [postal-mime ↗](https://www.npmjs.com/package/postal-mime) to parse the raw email:

* [  JavaScript ](#tab-panel-3406)
* [  TypeScript ](#tab-panel-3407)

JavaScript

```

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log("Subject:", parsed.subject);

    console.log("Text body:", parsed.text);

    console.log("HTML body:", parsed.html);

    console.log("Attachments:", parsed.attachments);

  }

}


```

TypeScript

```

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log("Subject:", parsed.subject);

    console.log("Text body:", parsed.text);

    console.log("HTML body:", parsed.html);

    console.log("Attachments:", parsed.attachments);

  }

}


```

### Detecting auto-reply emails

Use `isAutoReplyEmail()` to detect auto-reply emails and avoid mail loops:

* [  JavaScript ](#tab-panel-3410)
* [  TypeScript ](#tab-panel-3411)

JavaScript

```

import { isAutoReplyEmail } from "agents/email";

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    // Detect auto-reply emails to avoid sending duplicate responses

    if (isAutoReplyEmail(parsed.headers)) {

      console.log("Skipping auto-reply email");

      return;

    }


    // Process the email...

  }

}


```

TypeScript

```

import { isAutoReplyEmail } from "agents/email";

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    // Detect auto-reply emails to avoid sending duplicate responses

    if (isAutoReplyEmail(parsed.headers)) {

      console.log("Skipping auto-reply email");

      return;

    }


    // Process the email...

  }

}


```

This checks for standard RFC 3834 headers (`Auto-Submitted`, `X-Auto-Response-Suppress`, `Precedence`) that indicate an email is an auto-reply.

### Replying to emails

Use `this.replyToEmail()` to send a reply through the inbound email's reply channel:

* [  JavaScript ](#tab-panel-3412)
* [  TypeScript ](#tab-panel-3413)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    await this.replyToEmail(email, {

      fromName: "Support Bot", // Display name for the sender

      subject: "Re: Your inquiry", // Optional, defaults to "Re: "

      body: "Thanks for contacting us!", // Email body

      contentType: "text/plain", // Optional, defaults to "text/plain"

      headers: {

        // Optional custom headers

        "X-Custom-Header": "value",

      },

      secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing

    });

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    await this.replyToEmail(email, {

      fromName: "Support Bot", // Display name for the sender

      subject: "Re: Your inquiry", // Optional, defaults to "Re: "

      body: "Thanks for contacting us!", // Email body

      contentType: "text/plain", // Optional, defaults to "text/plain"

      headers: {

        // Optional custom headers

        "X-Custom-Header": "value",

      },

      secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing

    });

  }

}


```

### Deferred replies

`replyToEmail()` requires a live `AgentEmail` object, so it only works inside `onEmail()`. If you need to reply later — from a scheduled task, a callable method, or after a human-in-the-loop approval — store the sender info in state and use `sendEmail()`:

* [  JavaScript ](#tab-panel-3424)
* [  TypeScript ](#tab-panel-3425)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    this.setState({

      ...this.state,

      pendingReply: {

        to: email.from,

        messageId: parsed.messageId,

        subject: parsed.subject,

      },

    });

  }


  @callable()

  async sendDelayedReply(body) {

    const { pendingReply } = this.state;

    if (!pendingReply) return;


    await this.sendEmail({

      binding: this.env.EMAIL,

      to: pendingReply.to,

      from: "support@yourdomain.com",

      subject: `Re: ${pendingReply.subject}`,

      text: body,

      inReplyTo: pendingReply.messageId,

      secret: this.env.EMAIL_SECRET,

    });

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    this.setState({

      ...this.state,

      pendingReply: {

        to: email.from,

        messageId: parsed.messageId,

        subject: parsed.subject,

      },

    });

  }


  @callable()

  async sendDelayedReply(body: string) {

    const { pendingReply } = this.state;

    if (!pendingReply) return;


    await this.sendEmail({

      binding: this.env.EMAIL,

      to: pendingReply.to,

      from: "support@yourdomain.com",

      subject: `Re: ${pendingReply.subject}`,

      text: body,

      inReplyTo: pendingReply.messageId,

      secret: this.env.EMAIL_SECRET,

    });

  }

}


```

The `inReplyTo` field sets the `In-Reply-To` header so mail clients thread the reply correctly. The `secret` signs the agent routing headers so that follow-up replies route back to this agent instance via `createSecureReplyEmailResolver`.

### Forwarding emails

* [  JavaScript ](#tab-panel-3408)
* [  TypeScript ](#tab-panel-3409)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    await email.forward("admin@example.com");

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    await email.forward("admin@example.com");

  }

}


```

### Rejecting emails

* [  JavaScript ](#tab-panel-3414)
* [  TypeScript ](#tab-panel-3415)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    if (isSpam(email)) {

      email.setReject("Message rejected as spam");

      return;

    }

    // Process the email...

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    if (isSpam(email)) {

      email.setReject("Message rejected as spam");

      return;

    }

    // Process the email...

  }

}


```

## Error handling

When sending emails via `sendEmail()` or `replyToEmail()`, handle these common errors:

* [  JavaScript ](#tab-panel-3426)
* [  TypeScript ](#tab-panel-3427)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    try {

      await this.replyToEmail(email, {

        fromName: "Support Bot",

        body: "Thanks for your email!",

      });

    } catch (error) {

      switch (error.code) {

        case "E_SENDER_NOT_VERIFIED":

          console.error("Sender domain not verified. Verify in dashboard.");

          break;

        case "E_RATE_LIMIT_EXCEEDED":

          console.error("Rate limit exceeded. Back off and retry.");

          break;

        case "E_DAILY_LIMIT_EXCEEDED":

          console.error("Daily sending quota reached.");

          break;

        case "E_CONTENT_TOO_LARGE":

          console.error("Email content exceeds size limit.");

          break;

        default:

          console.error("Email sending failed:", error.message);

      }

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    try {

      await this.replyToEmail(email, {

        fromName: "Support Bot",

        body: "Thanks for your email!",

      });

    } catch (error) {

      switch (error.code) {

        case "E_SENDER_NOT_VERIFIED":

          console.error("Sender domain not verified. Verify in dashboard.");

          break;

        case "E_RATE_LIMIT_EXCEEDED":

          console.error("Rate limit exceeded. Back off and retry.");

          break;

        case "E_DAILY_LIMIT_EXCEEDED":

          console.error("Daily sending quota reached.");

          break;

        case "E_CONTENT_TOO_LARGE":

          console.error("Email content exceeds size limit.");

          break;

        default:

          console.error("Email sending failed:", error.message);

      }

    }

  }

}


```

### Common error codes

| Error Code                 | Description                        | Solution                             |
| -------------------------- | ---------------------------------- | ------------------------------------ |
| E\_SENDER\_NOT\_VERIFIED   | Sender domain/address not verified | Verify in Cloudflare dashboard       |
| E\_RATE\_LIMIT\_EXCEEDED   | Sending rate limit reached         | Implement exponential backoff        |
| E\_DAILY\_LIMIT\_EXCEEDED  | Daily quota exceeded               | Wait for quota reset or upgrade plan |
| E\_CONTENT\_TOO\_LARGE     | Email exceeds size limit           | Reduce attachments or content        |
| E\_RECIPIENT\_NOT\_ALLOWED | Recipient not in allowed list      | Check allowed destination addresses  |
| E\_RECIPIENT\_SUPPRESSED   | Recipient is on suppression list   | Remove from suppression list         |
| E\_VALIDATION\_ERROR       | Invalid email format               | Check email addresses                |
| E\_TOO\_MANY\_RECIPIENTS   | More than 50 recipients            | Split into multiple sends            |

## Secure reply routing

When your agent sends emails and expects replies, use secure reply routing to prevent attackers from forging headers to route emails to arbitrary agent instances.

### How it works

1. **Outbound:** When you call `replyToEmail()` or `sendEmail()` with a `secret`, the agent signs the routing headers (`X-Agent-Name`, `X-Agent-ID`) using HMAC-SHA256.
2. **Inbound:** `createSecureReplyEmailResolver` verifies the signature before routing.
3. **Enforcement:** If an email was routed via the secure resolver, `replyToEmail()` requires a secret (or explicit `null` to opt-out).

### Setup

1. Add a secret to your Worker:  
   * [  wrangler.jsonc ](#tab-panel-3394)  
   * [  wrangler.toml ](#tab-panel-3395)  
JSONC  
```  
{  
  "$schema": "./node_modules/wrangler/config-schema.json",  
  "vars": {  
    "EMAIL_SECRET": "change-me-in-production"  
  }  
}  
```  
TOML  
```  
[vars]  
EMAIL_SECRET = "change-me-in-production"  
```  
For production, use Wrangler secrets instead:  
Terminal window  
```  
npx wrangler secret put EMAIL_SECRET  
```
2. Use the combined resolver pattern:  
   * [  JavaScript ](#tab-panel-3422)  
   * [  TypeScript ](#tab-panel-3423)  
JavaScript  
```  
export default {  
  async email(message, env) {  
    const secureReplyResolver = createSecureReplyEmailResolver(  
      env.EMAIL_SECRET,  
    );  
    const addressResolver = createAddressBasedEmailResolver("EmailAgent");  
    await routeAgentEmail(message, env, {  
      resolver: async (email, env) => {  
        const replyRouting = await secureReplyResolver(email, env);  
        if (replyRouting) return replyRouting;  
        return addressResolver(email, env);  
      },  
    });  
  },  
};  
```  
TypeScript  
```  
export default {  
  async email(message, env) {  
    const secureReplyResolver = createSecureReplyEmailResolver(  
      env.EMAIL_SECRET,  
    );  
    const addressResolver = createAddressBasedEmailResolver("EmailAgent");  
    await routeAgentEmail(message, env, {  
      resolver: async (email, env) => {  
        const replyRouting = await secureReplyResolver(email, env);  
        if (replyRouting) return replyRouting;  
        return addressResolver(email, env);  
      },  
    });  
  },  
} satisfies ExportedHandler<Env>;  
```
3. Sign outbound emails:  
   * [  JavaScript ](#tab-panel-3420)  
   * [  TypeScript ](#tab-panel-3421)  
JavaScript  
```  
class MyAgent extends Agent {  
  async onEmail(email) {  
    await this.replyToEmail(email, {  
      fromName: "My Agent",  
      body: "Thanks for your email!",  
      secret: this.env.EMAIL_SECRET, // Signs the routing headers  
    });  
  }  
}  
```  
TypeScript  
```  
class MyAgent extends Agent {  
  async onEmail(email: AgentEmail) {  
    await this.replyToEmail(email, {  
      fromName: "My Agent",  
      body: "Thanks for your email!",  
      secret: this.env.EMAIL_SECRET, // Signs the routing headers  
    });  
  }  
}  
```

### Enforcement behavior

When an email is routed via `createSecureReplyEmailResolver`, the `replyToEmail()` method enforces signing:

| secret value        | Behavior                                                     |
| ------------------- | ------------------------------------------------------------ |
| "my-secret"         | Signs headers (secure)                                       |
| undefined (omitted) | **Throws error** \- must provide secret or explicit opt-out  |
| null                | Allowed but not recommended - explicitly opts out of signing |

## Complete example

Here is a complete Email Service agent that sends outbound mail and handles secure replies:

* [  JavaScript ](#tab-panel-3428)
* [  TypeScript ](#tab-panel-3429)

JavaScript

```

import { Agent, callable, routeAgentEmail } from "agents";

import {

  createAddressBasedEmailResolver,

  createSecureReplyEmailResolver,

} from "agents/email";

import PostalMime from "postal-mime";


export class EmailAgent extends Agent {

  @callable()

  async sendWelcome(to) {

    return this.sendEmail({

      binding: this.env.EMAIL,

      to,

      from: "support@yourdomain.com",

      subject: "Welcome!",

      text: "Thanks for signing up.",

      secret: this.env.EMAIL_SECRET,

    });

  }


  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log(`Email from ${email.from}: ${parsed.subject}`);


    const emails = this.state.emails || [];

    emails.push({

      from: email.from,

      subject: parsed.subject,

      receivedAt: new Date().toISOString(),

    });

    this.setState({ ...this.state, emails });


    await this.replyToEmail(email, {

      fromName: "Support Bot",

      body: `Thanks for your email! We received: "${parsed.subject}"`,

      secret: this.env.EMAIL_SECRET,

    });

  }

}


export default {

  async email(message, env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

      {

        maxAge: 7 * 24 * 60 * 60, // 7 days

        onInvalidSignature: (email, reason) => {

          console.warn(`Invalid signature from ${email.from}: ${reason}`);

        },

      },

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;

        return addressResolver(email, env);

      },

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

};


```

TypeScript

```

import { Agent, callable, routeAgentEmail } from "agents";

import {

  createAddressBasedEmailResolver,

  createSecureReplyEmailResolver,

  type AgentEmail,

} from "agents/email";

import PostalMime from "postal-mime";


interface Env {

  EmailAgent: DurableObjectNamespace<EmailAgent>;

  EMAIL: SendEmail;

  EMAIL_SECRET: string;

}


export class EmailAgent extends Agent<Env> {

  @callable()

  async sendWelcome(to: string) {

    return this.sendEmail({

      binding: this.env.EMAIL,

      to,

      from: "support@yourdomain.com",

      subject: "Welcome!",

      text: "Thanks for signing up.",

      secret: this.env.EMAIL_SECRET,

    });

  }


  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log(`Email from ${email.from}: ${parsed.subject}`);


    const emails = this.state.emails || [];

    emails.push({

      from: email.from,

      subject: parsed.subject,

      receivedAt: new Date().toISOString(),

    });

    this.setState({ ...this.state, emails });


    await this.replyToEmail(email, {

      fromName: "Support Bot",

      body: `Thanks for your email! We received: "${parsed.subject}"`,

      secret: this.env.EMAIL_SECRET,

    });

  }

}


export default {

  async email(message, env: Env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

      {

        maxAge: 7 * 24 * 60 * 60, // 7 days

        onInvalidSignature: (email, reason) => {

          console.warn(`Invalid signature from ${email.from}: ${reason}`);

        },

      },

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;

        return addressResolver(email, env);

      },

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

} satisfies ExportedHandler<Env>;


```

## API reference

### `sendEmail`

TypeScript

```

async sendEmail(options: {

  binding: EmailSendBinding;

  to: string | string[];

  from: string | { email: string; name?: string };

  subject: string;

  text?: string;

  html?: string;

  replyTo?: string | { email: string; name?: string };

  cc?: string | string[];

  bcc?: string | string[];

  inReplyTo?: string;

  headers?: Record<string, string>;

  secret?: string;

}): Promise<EmailSendResult>;


```

Send an outbound email through the Email Service binding. Automatically injects `X-Agent-Name` and `X-Agent-ID` headers. When `secret` is provided, signs headers with HMAC-SHA256 for secure reply routing.

| Option    | Description                                                               |
| --------- | ------------------------------------------------------------------------- |
| binding   | The send\_email binding (for example, this.env.EMAIL). Required.          |
| to        | Recipient address or array of addresses                                   |
| from      | Sender address, or \\{ email, name \\} object                             |
| subject   | Email subject line                                                        |
| text      | Plain text body (at least one of text/html required)                      |
| html      | HTML body (at least one of text/html required)                            |
| replyTo   | Reply-to address for the recipient                                        |
| cc        | CC recipient(s)                                                           |
| bcc       | BCC recipient(s)                                                          |
| inReplyTo | Message-ID for threading (sets the In-Reply-To header)                    |
| headers   | Additional custom headers (agent headers take precedence if they collide) |
| secret    | Secret for HMAC signing of agent routing headers                          |

### `routeAgentEmail`

TypeScript

```

function routeAgentEmail<Env>(

  email: ForwardableEmailMessage,

  env: Env,

  options: {

    resolver: EmailResolver;

    onNoRoute?: (email: ForwardableEmailMessage) => void | Promise<void>;

  },

): Promise<void>;


```

Routes an incoming email to the appropriate Agent based on the resolver's decision.

| Option    | Description                                                                                                                                                                             |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| resolver  | Function that determines which agent to route the email to                                                                                                                              |
| onNoRoute | Optional callback invoked when no routing information is found. Use this to reject the email or perform custom handling. If not provided, a warning is logged and the email is dropped. |

### `createSecureReplyEmailResolver`

TypeScript

```

function createSecureReplyEmailResolver(

  secret: string,

  options?: {

    maxAge?: number;

    onInvalidSignature?: (

      email: ForwardableEmailMessage,

      reason: SignatureFailureReason,

    ) => void;

  },

): EmailResolver;


type SignatureFailureReason =

  | "missing_headers"

  | "expired"

  | "invalid"

  | "malformed_timestamp";


```

Creates a resolver for routing email replies with signature verification.

| Option             | Description                                                              |
| ------------------ | ------------------------------------------------------------------------ |
| secret             | Secret key for HMAC verification (must match the key used to sign)       |
| maxAge             | Maximum age of signature in seconds (default: 30 days / 2592000 seconds) |
| onInvalidSignature | Optional callback for logging when signature verification fails          |

### `signAgentHeaders`

TypeScript

```

function signAgentHeaders(

  secret: string,

  agentName: string,

  agentId: string,

): Promise<Record<string, string>>;


```

Manually sign agent routing headers. Returns an object with `X-Agent-Name`, `X-Agent-ID`, `X-Agent-Sig`, and `X-Agent-Sig-Ts` headers.

Useful when sending emails through external services while maintaining secure reply routing. The signature includes a timestamp and will be valid for 30 days by default.

## Next steps

[ HTTP and SSE ](https://developers.cloudflare.com/agents/api-reference/http-sse/) Handle HTTP requests in your Agent. 

[ Webhooks ](https://developers.cloudflare.com/agents/guides/webhooks/) Receive events from external services. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/email/","name":"Email"}}]}
```
