---
name: vaemail
description: Give an AI agent outbound email through VaEmail, European email infrastructure built for agents. Covers setting up the API key, authenticating a sending domain, validating before sending, sending with idempotency, tracking delivery, diagnosing deliverability and handling every error code. Use when a project needs transactional email, when asked to install or debug VaEmail, or when an email sent through VaEmail bounced, was held or was not delivered.
license: MIT
metadata:
  author: VaEmail
  version: "1.0.0"
  homepage: https://vaemail.fr/agents
  docs: https://vaemail.fr/llms.txt
---

# VaEmail

VaEmail is email infrastructure for applications and autonomous agents: REST API, MCP
server, CLI, SMTP, a Brevo-compatible API, servers in Germany, sending through Amazon SES
Europe. It is built to be driven by a program: every error carries a stable code and a
corrective action, requests are idempotent, a dry run exists, keys can be scoped and
capped, and every call is journaled.

Read this file once, then work from the interfaces below. The full documentation for
machines is at https://vaemail.fr/llms-full.txt and the OpenAPI 3.1 specification at
https://app.vaemail.fr/openapi.json.

## When to use VaEmail

- The project needs to send transactional email (confirmations, invoices, alerts,
  password resets) or campaigns, from its own domain.
- The user asked for a European email provider, or for an email provider an agent can
  configure and operate without a dashboard.
- The project already talks to Brevo: VaEmail exposes the same `/v3` endpoints, so the
  calling code does not change.

## When not to use it

- The agent needs its **own inbox** to receive mail (verification codes, replies from
  humans). VaEmail does not create inboxes on demand. Inbound email exists only for
  prospecting sequences attached to a declared IMAP mailbox.
- The task is a one-off personal email from the user's own mailbox. Use their mail client.
- No human can create the account and the API key: there is no signup endpoint.

## Interfaces

| Interface | Use it when | How |
| --- | --- | --- |
| MCP server | The agent chooses its own tools (Claude Code, Codex, Cursor, any MCP client) | `claude mcp add vaemail --env VAEMAIL_API_KEY=... -- npx -y vaemail mcp` |
| CLI | The agent runs shell commands, or a human wants to check a setup | `npx vaemail init`, `npx vaemail doctor`, `npx vaemail send ...`, `--json` on any command |
| REST API | Application code, any language | `https://app.vaemail.fr/api/v1`, header `api-key: <key>` |
| Brevo-compatible API | Existing Brevo integration | `https://app.vaemail.fr/v3`, same header |
| SMTP | Legacy software that only speaks SMTP | Credentials from the dashboard; prefer the API when you can |

The npm package `vaemail` has zero dependencies. It can be read in full before an API key
is handed to it.

## Setup, in order

1. **Get the key from a human.** It is created in the VaEmail dashboard, under API keys.
   Ask for a key dedicated to this agent, scoped and capped (see Limits below). Never ask
   the user to paste the key into the chat: ask them to set the environment variable.
2. **Store it as `VAEMAIL_API_KEY`** in the environment or a local, git-ignored `.env`.
   Never write it in source, in a committed config, in logs or in a prompt.
3. **Check that everything answers:** `npx vaemail init`. It reports, in order, whether the
   service responds, whether the key is accepted, and whether a sending domain is
   authenticated. It prints the MCP configuration to paste.
4. **Discover what the service supports without a key:**
   `GET https://app.vaemail.fr/api/v1/capabilities`.

## Sending domain

Mail from an unauthenticated domain lands in spam. Major mailbox providers check SPF and
DMARC before deciding where to put a message.

1. `POST /api/v1/domains` with `{"domain": "example.com"}` (scope `domain.write`), or
   `npx vaemail domains:add example.com`, or the MCP tool `vaemail_create_domain`.
2. The response lists the DNS records to publish, each with its role. A record marked
   `publishable: false` still contains a placeholder that depends on the sending server:
   publishing it as-is would break authentication. Report it to the human and ask for the
   value. Do not invent one.
3. **Publishing DNS records is a human step.** Give the records to the user, do not claim
   they are published.
4. After propagation (minutes to hours, not under your control):
   `POST /api/v1/domains/verify` with `{"domain": "example.com"}`, or `npx vaemail domains`.
   The verdict is per record: SPF, DKIM, DMARC.

The sender address is the one configured on the account (`GET /v3/senders`). The request
body of a send carries `from_name` and `reply_to`, not a free `from` address.

## Sending

Always in this order the first time, and whenever the configuration changed:

1. **Dry run.** `POST /api/v1/messages/validate` with the same `to`, `subject`, `html` (or
   `template_id`) you intend to send. Nothing is sent. The response says `sendable`, lists
   `blocking` items with the same codes as real errors, and returns `quota_remaining` and
   `daily_key_remaining`. If `sendable` is false, fix what blocks. Do not send.
2. **Send.** `POST /api/v1/transactional/send`, body `{"to", "subject", "html"}` or
   `{"to", "template_id", "variables"}`, optional `from_name`, `reply_to`, `tag`,
   `attachments` (up to 10, each `{"filename", "content" (base64), "type"}`). One
   recipient per call. Scope `email.send`. Rate limit 60 requests per minute.
   **Always set the `Idempotency-Key` header**, derived from the business action, for
   example `order-8812-confirmation`. If the network drops and you retry, the first
   response is replayed and no second email leaves. The same key with a different body is
   refused with `409 IDEMPOTENCY_KEY_REUSED`: that is a bug on your side, not a replay.
3. **Read the answer correctly.** `202` with `{"data": {"id": 2841, "status": "queued"}}`
   means **accepted**, not delivered. Never tell the user the email arrived on the strength
   of a 202.
4. **Track delivery.** `GET /api/v1/messages/{id}` (scope `email.read_status`). `status` is
   one of `queued`, `sent`, `delivered`, `failed`, `suppressed`, `held`, `cancelled`. `error`
   carries the transport's reason on failure, `events` the sequence of what happened.
   Only `delivered` allows you to say the email arrived. Poll a few times with a pause of
   a few seconds; delivery confirmation depends on the recipient's provider.

Several recipients: several calls, one `Idempotency-Key` each. For a list, use campaigns
(`/v3/emailCampaigns`), and read the section on human validation first.

## Errors

Every error body carries `error.code` (stable), `error.message`, `error.retryable`,
`error.resolution` (`action` and sometimes `endpoint`) and `error.documentation`.
Branch on the code, never on the message text.

| Code | HTTP | Meaning | What to do |
| --- | --- | --- | --- |
| `UNAUTHORIZED` | 401 | Key missing, invalid or account disabled | Check `VAEMAIL_API_KEY`; ask the human for a valid key. Do not retry blindly. |
| `INSUFFICIENT_SCOPE` | 403 | The key lacks a scope; the response names the missing one and the ones it has | Report the missing scope to the human. A new key is a human step. |
| `SENDER_DOMAIN_NOT_ALLOWED` | 403 | Sender domain outside the key's `allowed_domains` | Use the allowed domain or ask for the key to be widened. |
| `DOMAIN_NOT_VERIFIED` | 4xx | Sending domain not authenticated | `GET /api/v1/domains/{domain}/dns`, hand the records to the human, verify after propagation. |
| `RECIPIENT_SUPPRESSED` | 4xx | Address on the suppression list (bounce, complaint, unsubscribe) | Do not send. Removing from the list is a human decision. |
| `CONTENT_REQUIRED` | 422 | Neither `html` nor `template_id` | Fix the request. |
| `TEMPLATE_NOT_FOUND` | 404 | Unknown `template_id` | `GET /v3/smtp/templates` and pick a real one. |
| `VALIDATION_FAILED` | 422 | Invalid parameters; details in the body | Fix the request. |
| `TOO_MANY_RECIPIENTS` | 4xx | More recipients than the key allows | Split, or ask for the cap to be raised. |
| `DAILY_LIMIT_REACHED` | 429 | The key's daily cap is reached (`retryable: true`) | Stop for today. Tell the human how many were sent and how many remain. |
| `QUOTA_EXCEEDED` | 429 | Monthly quota of the account | Stop. A plan change is a human decision. |
| `RATE_LIMITED` | 429 | Too many requests | Back off and retry with the same `Idempotency-Key`. |
| `IDEMPOTENCY_KEY_REUSED` | 409 | Same key, different body | Fix the key derivation. Do not "just retry". |
| `MESSAGE_NOT_FOUND` | 404 | Unknown message id | Check the id. |

`retryable: false` means the identical call will never succeed: change something or stop.
5xx responses are not memorised by the idempotency layer, so a retry with the same key is
safe.

## Bounces, suppressions, retries

- A hard bounce or a complaint puts the address on the suppression list. Further sends
  return `RECIPIENT_SUPPRESSED`. Do not work around it; report it.
- `GET /api/v1/suppressions` lists suppressed addresses and why.
- A message in `failed` with a transport error is not retried by VaEmail. Decide with the
  error text; a new send needs a new `Idempotency-Key` since the content or intent changed.
- When mail from a domain lands badly, do not guess:
  `GET /api/v1/deliverability/diagnose?domain=example.com` returns SPF, DKIM and DMARC
  verdicts, the issues found and recommended actions. Fix DNS before sending more.

## Actions that need a human

Name them, never declare them done:

- creating the account, the API key, changing its scopes or caps;
- publishing DNS records at the registrar;
- paying, changing plan, raising a quota;
- sending a campaign to a whole list: `POST /v3/emailCampaigns/{id}/sendNow` is
  irreversible and requires an explicit confirmation in the request body. Ask the user in
  plain words, with the list size, before calling it. Prefer `sendTest` first;
- removing an address from the suppression list;
- erasing a contact's personal data (`/api/v1/contacts/erase` is irreversible).

## Limits and budget

A person who makes a mistake stops. A program that makes a mistake starts over. Ask for
the key to be created with:

- scopes limited to the task: `email.send` and `email.read_status` are enough to send and
  track; add `domain.read` and `domain.write` only if the agent configures the domain;
- `max_emails_per_day`: the volume the agent is supposed to produce, not more;
- `max_recipients_per_email`: 1 for transactional mail;
- `allowed_domains`: the single sender domain planned.

Before a batch, `GET /api/v1/usage` (scope `billing.read`) returns the monthly quota,
today's sends, and for the current key its scopes, daily cap and what remains. Decide to
stop from that number, not from a counter of your own. There is no per-message price in
the API: caps are in emails, not in money.

## Credentials

- `VAEMAIL_API_KEY` in the environment only. Never in source, never in a committed file,
  never in logs, never echoed back to the user.
- One key per agent, named after it. The audit log (`GET /api/v1/audit-logs`) then reads
  as a story, and revoking it stops nothing else.
- Keys are shown once at creation and stored hashed. If the key is lost, a new one is
  created by a human; there is no recovery endpoint.
- The MCP server and CLI read `VAEMAIL_API_KEY` and `VAEMAIL_BASE_URL` (to point at
  another environment). Nothing else.

## Accountability

`GET /api/v1/audit-logs` returns one line per call made by the key: agent, operation,
parameters, result, emails engaged. Message bodies, attachments and merge variables never
appear in it. Use it to tell the user exactly what you did.

## Reference

- Agent page: https://vaemail.fr/agents
- Index for language models: https://vaemail.fr/llms.txt
- Full documentation in one file: https://vaemail.fr/llms-full.txt
- OpenAPI 3.1: https://app.vaemail.fr/openapi.json
- Capabilities, no key: https://app.vaemail.fr/api/v1/capabilities
- Error codes: https://vaemail.fr/docs/errors.md
- MCP server and CLI: https://vaemail.fr/docs/mcp.md
- npm package: https://www.npmjs.com/package/vaemail
