# VaEmail for OpenAI Codex

VaEmail is European email infrastructure, servers in Germany and sending in Europe, that an OpenAI Codex session can operate on its own: declare a sending domain, dry run a message, send it with an idempotency key, then read the delivery status. It attaches to Codex as a local MCP server (`npx -y vaemail mcp`, eighteen tools, no dependencies), or as a CLI when the session prefers shell commands. A send answers 202, which means accepted, not delivered. Creating the account, publishing DNS records and paying stay human steps.

Facts checked against the VaEmail documentation on 6 September 2026. Competitor facts come from https://vaemail.fr/compare/index.md, read against their official documentation on 6 September 2026; that page cites the URL for each claim.

## MCP configuration for the Codex CLI

Add the server to `~/.codex/config.toml`:

```toml
[mcp_servers.vaemail]
command = "npx"
args = ["-y", "vaemail", "mcp"]
env = { VAEMAIL_API_KEY = "swm_your_key" }
```

`~/.codex/config.toml` sits outside the repository, which is why the key can live there. It must never be written into a project file, a prompt or a log. A shell that already exports `VAEMAIL_API_KEY` covers the CLI path.

The transport is stdio: the server runs beside the Codex process, with no remote endpoint and no OAuth flow. The npm package `vaemail` has zero dependencies, so it can be read in full before an API key is handed to it. Node 18 or later, for the built-in `fetch`, and nothing else. Registry entry: `io.github.vaemail/vaemail`.

Getting the key is not something Codex can do: it is created from the VaEmail dashboard, under "Clés API", and there is no signup endpoint. Ask the user to export it as `VAEMAIL_API_KEY` rather than pasting it into the chat. `VAEMAIL_BASE_URL` points the package elsewhere than production; it defaults to `https://app.vaemail.fr`.

## The eighteen tools

| Tool | What it does |
| --- | --- |
| `vaemail_preparer_envoi` | Prepare a newsletter to a list: returns a summary and a token, nothing is sent. |
| `vaemail_confirmer_envoi` | Run the prepared send after explicit human approval. |
| `vaemail_ou_en_est_ma_campagne` | Where a campaign stands, in plain sentences. |
| `vaemail_qui_est_abonne` | Lists with their subscribers, or the lists and status of one address. |
| `vaemail_importer_contacts` | Add contacts to a list. Never resubscribes an unsubscribed address. |
| `vaemail_capabilities` | What the service supports. No API key needed. |
| `vaemail_send_email` | Queue a transactional email, return its id. |
| `vaemail_validate_email` | Dry run: would this send go out, and what would block it. |
| `vaemail_get_message` | Delivery status and every event for one message. |
| `vaemail_list_messages` | Recent messages, filtered by status, tag or recipient. |
| `vaemail_list_domains` | Sending domains with live SPF, DKIM and DMARC state. |
| `vaemail_create_domain` | Declare a domain, return the DNS records to publish. |
| `vaemail_verify_domain` | Re-read the DNS and report what is authenticated. |
| `vaemail_dns_requirements` | The records a declared domain still needs. |
| `vaemail_diagnose_deliverability` | Why mail is landing badly, with the actions that fix it. |
| `vaemail_list_bounces` | Addresses excluded from sending, and why. |
| `vaemail_get_usage` | Quota, daily cap on the key, what is left. |
| `vaemail_get_audit_log` | What this key has done, to report it accurately. |

Read tools carry the `readOnlyHint` annotation, so a client can tell which ones are safe to call without asking. An API error does not surface as a transport failure: it returns as a tool result marked in error, with the stable code, the corrective action and whether a retry can help.

## The CLI, for a session that runs commands

The same package installs a `vaemail` binary.

| Command | Use |
| --- | --- |
| `vaemail init` | Service reachable, key accepted, sending domain authenticated. Prints the MCP snippet. |
| `vaemail doctor` | Deliverability diagnosis: SPF, DKIM, DMARC, findings, recommended actions. Account-wide by default, `--domain <d>` narrows it to one domain. |
| `vaemail usage` | Monthly quota, sends today, what is left on the key. |
| `vaemail domains` | Declared domains and each record's state. `vaemail domains:add <domain>` declares one and prints the DNS records. |
| `vaemail messages` | Recent messages, filtered with `--status <s>`. `vaemail status <id>` gives one message and its events. |
| `vaemail send --to <a> --subject <s> --html <h>` | Queue one message. Prints the id and reminds that accepted is not delivered. |
| `vaemail capabilities` | What the service supports. No API key needed. |

`--json` on any command produces machine-readable output, and `--key` and `--base` override the two environment variables.

## Copy this to your agent

```
Integrate VaEmail into this project as the outbound email provider.

You have the VaEmail MCP server configured in ~/.codex/config.toml; use its tools
when they exist, the CLI (`npx vaemail ...`) otherwise.

Official documentation for agents: 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

Rules:
- The API key is in the environment variable VAEMAIL_API_KEY. Do not ask for it, never
  write it to a versioned file.
- Before any real send, call POST /api/v1/messages/validate and stop if "sendable" is false.
- Put an Idempotency-Key header on every send, derived from the business action.
- Through the vaemail MCP tools, pass idempotency_key on vaemail_send_email and read
  the outcome with vaemail_get_message.
- A 202 means "accepted", not "delivered": read GET /api/v1/messages/{id} before you
  report a delivery.
- Publishing DNS records, creating an account or paying are human steps: name them, never
  declare them done.

Start with `npx vaemail init`, or the `vaemail_capabilities` tool if you prefer not to
shell out, and tell me what it reports.
```

## The sequence that survives a retry

The agent skill carries the same rules in longer form, plus the full error table. Install it with `npx skills add vaemail/skills`.

Validate first. `POST /api/v1/messages/validate` takes the same body as the real call, `to` with `subject` and `html` or with `template_id` (only `to` is required), sends nothing, consumes nothing, and answers `sendable` with a `blocking` list using the same codes as real errors, plus `quota_remaining` and `daily_key_remaining`.

```bash
curl -X POST https://app.vaemail.fr/api/v1/messages/validate \
  -H "api-key: $VAEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"client@exemple.fr","html":"<p>Bonjour</p>"}'
```

Then send:

```bash
curl -X POST https://app.vaemail.fr/api/v1/transactional/send \
  -H "api-key: $VAEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: facture-2412" \
  -d '{"to":"client@exemple.fr","subject":"Votre facture","html":"<p>Bonjour</p>"}'
```

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

Through the MCP server the same guarantee is the `idempotency_key` parameter of `vaemail_send_email`, and `vaemail_validate_email` is the dry run.

That is a 202: queued, nothing more. `Idempotency-Key` memorises the response for 24 hours and replays it with `Idempotent-Replay: true`. The same key with a different body is refused by a 409 `IDEMPOTENCY_KEY_REUSED`, a bug in your key derivation rather than a replay. Server errors (5xx) are not memorised, so retrying them with the same key is safe.

Read the outcome on `GET /api/v1/messages/{id}`.

```bash
curl https://app.vaemail.fr/api/v1/messages/2841 \
  -H "api-key: $VAEMAIL_API_KEY"
```

`status` is one of `queued`, `sent`, `delivered`, `failed`, `suppressed`, `held`, `cancelled`. `queued`, then `sent`, then `delivered` is the successful path; the other four are terminal, so stop polling when you hit one. Only `delivered` justifies telling the user the mail arrived. The send endpoint is capped at 60 requests per minute and needs scope `email.send`; reading a status needs `email.read_status`.

## Guardrails to ask for when the key is created

A person who makes a mistake stops. A program that makes a mistake starts over, so bound the key at creation: scopes limited to the task: `email.send` and `email.read_status` to send and track, `analytics.read` to run `vaemail doctor` and read the audit log, `contacts.read` to read bounces, `billing.read` to read what is left, `domain.read` and `domain.write` only if the agent configures DNS, `max_emails_per_day` at the expected volume, `max_recipients_per_email` at 1 for transactional mail, `allowed_domains` restricted to the planned sender. Eight scopes exist. All of it is optional: a key that declares no scope and no cap has every scope and no ceiling, which is how keys created before these guardrails behave. `GET /api/v1/usage` returns the current key's scopes and daily cap, so check what you were actually given rather than assuming. An out-of-scope call returns 403 `INSUFFICIENT_SCOPE` naming the missing scope and the ones the key holds.

`GET /api/v1/usage` (scope `billing.read`) returns what is left. `GET /api/v1/audit-logs` (scope `analytics.read`) returns one line per call: which key, which operation, which parameters, which result, how many emails engaged. Message bodies, attachments and merge variables never appear in it.

Several actions are irreversible and need a human in plain words. `POST /v3/emailCampaigns/{id}/sendNow` ships to a whole list and requires an explicit confirmation in the request body; it sits on the Brevo-compatible `/v3` surface, no MCP tool reaches it, so an agent gets there only by calling the API directly, and a test should go out first with `sendTest`. `POST /api/v1/contacts/erase` deletes personal data for good. The OpenAPI specification marks each one `x-vaemail-destructive: true`, and the API reference flags them. Creating the account and the key, publishing DNS records, paying or raising a quota, and removing an address from the suppression list are human steps too. That guardrail exists because on 5 September 2026 a call with no body sent 2,982 emails on a Saturday evening.

## Where VaEmail is behind

Be honest with the user about the gaps. VaEmail creates no inbox on the fly, so a Codex session that needs to *receive* a verification code or a human reply is on the wrong tool; inbound exists only for prospecting sequences attached to a declared IMAP mailbox. There is a Python SDK (`pip install vaemail`), and existing Brevo clients work unchanged against `/v3`. The npm MCP server runs locally. A separate remote connector at `mcp.vaemail.fr`, with OAuth, exposes 16 tools. Six webhook events (sent, delivered, bounce, complaint, opened, clicked), with a documented signature (https://vaemail.fr/docs/webhooks.md): Resend documents 19 events signed with Svix, Brevo 15, AgentMail 11, Postmark 7 unsigned. Documentation is partly in French. The product is young and run by an agency. Prices are public on https://vaemail.fr/tarifs/.

## Links

- Agent page: https://vaemail.fr/agents
- Index for language models: https://vaemail.fr/llms.txt, full documentation: https://vaemail.fr/llms-full.txt
- MCP server and CLI: https://vaemail.fr/docs/mcp.md
- Error codes: https://vaemail.fr/docs/errors.md
- Scopes, caps and audit log: https://vaemail.fr/docs/security.md
- OpenAPI 3.1: https://app.vaemail.fr/openapi.json, capabilities without a key: https://app.vaemail.fr/api/v1/capabilities
- Comparison with Resend, Brevo, Postmark and AgentMail: https://vaemail.fr/compare/index.md
- Agent skill: https://vaemail.fr/skill, source https://github.com/vaemail/skills
- Examples, Codex included: https://github.com/vaemail/examples, server source https://github.com/vaemail/vaemail-mcp
- Other runtimes: https://vaemail.fr/email-for-claude-code, https://vaemail.fr/email-for-openai-agents
- Going further: https://vaemail.fr/mcp-email-server, https://vaemail.fr/ai-agent-email-deliverability
- Getting started end to end: https://vaemail.fr/docs/quickstart.md, full endpoint reference: https://vaemail.fr/docs/api.md
