# Email for CrewAI agents

VaEmail gives a CrewAI crew outbound email through a local MCP server, started with `npx -y vaemail mcp` and loaded into the crew by `MCPServerAdapter` from `crewai-tools[mcp]`. The agent that owns those tools validates before sending, sets an idempotency key on every send, and reads the real delivery status before it reports anything. A send answers 202, which means accepted, not delivered. Servers are in Germany, sending is done in Europe.

Facts checked against the VaEmail documentation on 6 September 2026.

## The reference example

This is `examples/crewai/agent.py` from https://github.com/vaemail/examples, complete and runnable.

```python
"""A CrewAI agent that sends email through the VaEmail MCP server.

pip install crewai "crewai-tools[mcp]"
VAEMAIL_API_KEY=swm_... OPENAI_API_KEY=... python agent.py
"""
import os

from crewai import Agent, Crew, Task
from crewai_tools import MCPServerAdapter
from mcp import StdioServerParameters

params = StdioServerParameters(
    command="npx",
    args=["-y", "vaemail", "mcp"],
    env={**os.environ, "VAEMAIL_API_KEY": os.environ["VAEMAIL_API_KEY"]},
)

with MCPServerAdapter(params) as tools:
    mailer = Agent(
        role="Mailer",
        goal="Send email through VaEmail and report its real delivery status.",
        backstory=(
            "You validate before sending, you set an idempotency key, and you never "
            "report a delivery before vaemail_get_message says delivered."
        ),
        tools=tools,
        verbose=True,
    )
    task = Task(
        description=(
            "Send an email to client@example.com, subject 'Your invoice', "
            "body '<p>Hello, your invoice is attached.</p>', then report its delivery status."
        ),
        expected_output="The message id and its delivery status, in one sentence.",
        agent=mailer,
    )
    print(Crew(agents=[mailer], tasks=[task]).kickoff())
```

Three things in that file carry the weight. The transport is stdio, so the server runs as a child process of the Python process and the API key never leaves the machine. The whole crew runs inside the `with` block, because the tools only exist while the adapter holds the connection. And the operating rules live in the agent's `backstory`, where the model reads them on every turn, not in a comment.

## What the crew receives

`MCPServerAdapter` exposes the server's eighteen tools. An email task uses six of them.

| Tool | What it does |
| --- | --- |
| `vaemail_validate_email` | Dry run. Reports whether the send would go out and what would block it. Sends nothing. |
| `vaemail_send_email` | Queues a transactional email, returns its id. Takes `idempotency_key`. |
| `vaemail_get_message` | Delivery status and every event for one message, plus the transport error on failure. |
| `vaemail_diagnose_deliverability` | Why mail is landing badly, with the actions that fix it. |
| `vaemail_get_usage` | Quota, the key's daily cap, what is left. |
| `vaemail_capabilities` | What the service supports. No API key needed. |

The other twelve cover campaign preparation, contacts, domains, message listing, bounces and the audit log. All eighteen are described on https://vaemail.fr/docs/mcp.md and in the package README at https://github.com/vaemail/vaemail-mcp. Read tools carry the `readOnlyHint` annotation, so a client that gates writes behind approval can tell them apart. The npm package has zero dependencies and needs Node 18 for the built in `fetch`, which means you can read it in full before handing it a key.

## Validate before the first real send

`vaemail_validate_email` takes the same `to`, `subject` and `html` you intend to send and returns `sendable`, the `blocking` items with the same codes as real errors, `quota_remaining` and `daily_key_remaining`. Nothing is sent and nothing is consumed. A suppressed recipient shows up here rather than as a failed send later.

Put the rule in the agent's `backstory` and in the task description, not only in the code around the crew: the model is the one deciding whether to call the tool.

## Idempotency, because a crew reruns tasks

A crew can rerun a task after a timeout or an exception, and a model can call the same tool twice in one run. Pass `idempotency_key` on every `vaemail_send_email` call, derived from the business action rather than from a timestamp: `invoice-8812-reminder`, not `send-1757190000`.

The first response is memorised for 24 hours and replayed identically, with `Idempotent-Replay: true`, so a retried send does not leave twice. The same key with a different body is refused with `409 IDEMPOTENCY_KEY_REUSED`. That is a bug in how the key is derived, not a replay to work around. Server errors are not memorised, so retrying a 5xx with the same key is safe.

## Delivery status is a second call

`vaemail_send_email` returns an id and `status: "queued"`. The HTTP answer behind it is 202: the message is accepted and queued, and nothing about the recipient's mailbox is known yet.

```json
{ "success": true, "data": { "id": 2841, "status": "queued" } }
```

`vaemail_get_message` is what says what happened. `status` is one of `queued`, `sent`, `delivered`, `failed`, `suppressed`, `held`, `cancelled`; `events` carries the sequence, `error` the transport's reason on failure. Only `delivered` supports the sentence "the email arrived", which is why the example task asks for the status rather than for a confirmation. Delivery confirmation depends on the recipient's provider, so a task that must report it should poll a few times with a pause between calls.

## Errors the agent can act on

An API error does not come back to the crew as a transport failure. It comes back as a tool result marked in error, carrying the stable code, the corrective action and whether a retry can change anything.

```
RECIPIENT_SUPPRESSED: Destinataire dans la liste de suppression.
Action corrective : remove_from_suppression_list (DELETE /api/v1/suppressions/{email})
Réessayer à l'identique ne changera rien.
```

Branch on `code`, never on the message text. `DOMAIN_NOT_VERIFIED` means the sending domain has no SPF and DMARC published, so hand the records to a human instead of retrying. `INSUFFICIENT_SCOPE` names the missing scope and the ones the key holds. `DAILY_LIMIT_REACHED` is marked `retryable: true` because the cap resets, but the right move is to stop for the day and say how many were sent. `retryable: false` means the identical call will never succeed. The full list is at https://vaemail.fr/docs/errors.md.

## A key scoped to this crew

Create a key named after the crew and bound at creation: scopes `email.send` and `email.read_status` are enough to send and track, `max_emails_per_day` set to the volume the crew is supposed to produce, `max_recipients_per_email` at 1 for transactional mail, and `allowed_domains` holding the single sender domain. There are eight scopes in total; a key that declares none has them all. An agent that configures the domain itself also needs `domain.read` and `domain.write`.

Transactional sends are rate limited to 60 requests per minute. `vaemail_get_usage` returns what remains, which is how a long crew run decides to stop from a real number rather than from a counter of its own. `GET /api/v1/audit-logs` returns one line per call, message bodies excluded, so the run can be audited afterwards.

Creating the account and the key is a human step. There is no signup endpoint.

## Limits worth knowing before you build on it

VaEmail does not create an inbox on demand, so a crew that needs to receive a verification code or a human reply cannot get an address from the API. Inbound exists only for prospecting sequences attached to a declared IMAP mailbox. In Python you can use the SDK (`pip install vaemail`), go through the MCP server, as above, or call the REST API directly, which is what `examples/python` does with the standard library only. Existing Brevo clients work against `/v3` unchanged.

MCP comes as two servers: a remote connector at `mcp.vaemail.fr` with OAuth (16 tools) and the local npm package over stdio (18 tools, zero dependencies). Six webhook events are emitted (sent, delivered, bounce, complaint, opened, clicked) against fifteen to nineteen elsewhere; the signature is documented on https://vaemail.fr/docs/webhooks.md. Part of the documentation is in French. The product is young and run by an agency, not by a funded company. Pricing is public on https://vaemail.fr/tarifs/.

Those comparisons come from the official documentation of each provider read on 6 September 2026, with the exact URLs cited on https://vaemail.fr/compare/index.md.

## Copy this to your agent

```
Add VaEmail to this crew as the outbound email provider.
Agent documentation: https://vaemail.fr/llms.txt
OpenAPI specification: https://app.vaemail.fr/openapi.json
What the service supports, no key needed: https://app.vaemail.fr/api/v1/capabilities

Wire it as an MCP server through crewai_tools.MCPServerAdapter, with
StdioServerParameters(command="npx", args=["-y", "vaemail", "mcp"]) and
VAEMAIL_API_KEY passed from os.environ. Build the crew inside the adapter's
context manager. Put these rules in the backstory of the agent that holds the tools:

- The API key is in the environment variable VAEMAIL_API_KEY. Do not ask for it,
  never write it into a versioned file, never echo it back.
- Before any real send, call vaemail_validate_email and stop if "sendable" is false.
- Set idempotency_key on every vaemail_send_email call, derived from the business
  action, so a rerun of the task does not send the message twice.
- A send is accepted, not delivered: read vaemail_get_message and report a delivery
  only when status is "delivered".
- When mail lands badly, call vaemail_diagnose_deliverability instead of guessing.
- Publishing DNS records, creating an account and paying are human steps: name them,
  never declare them done.
```

Check the wiring before the first run with `npx vaemail init`, which reports whether the service answers, whether the key is accepted, and whether a sending domain is authenticated.

## Links

- Agent page: https://vaemail.fr/agents
- Full documentation in one file: https://vaemail.fr/llms-full.txt
- MCP server and CLI, all eighteen tools: https://vaemail.fr/docs/mcp.md
- Error codes and what to do for each: https://vaemail.fr/docs/errors.md
- Scopes, caps, idempotency, audit log: https://vaemail.fr/docs/security.md
- Authenticating a sending domain: https://vaemail.fr/docs/domains.md
- API reference: https://vaemail.fr/docs/api.md
- OpenAPI 3.1: https://app.vaemail.fr/openapi.json
- Capabilities, no key: https://app.vaemail.fr/api/v1/capabilities
- Agent skill: https://vaemail.fr/skill
- Examples, including this one: https://github.com/vaemail/examples
- MCP server source: https://github.com/vaemail/vaemail-mcp
- Comparison with Resend, Brevo, Postmark and AgentMail: https://vaemail.fr/compare/index.md
- The same wiring for LangChain: https://vaemail.fr/email-for-langchain
- The same wiring for the OpenAI Agents SDK: https://vaemail.fr/email-for-openai-agents
- The MCP server on its own: https://vaemail.fr/mcp-email-server
- What an autonomous agent needs before it is given a key: https://vaemail.fr/autonomous-agent-email
