# Autonomous agent email: autonomy, reliability, safety, explainability

An agent fails differently from a person. It retries, it does not read a dashboard, and it reports success from an HTTP 200. VaEmail is shaped around that: an agent runs the whole setup and send loop on its own, retries do not duplicate, a key cannot engage more than it was given, and every call is journaled for a human to read afterwards.

Facts checked against the VaEmail documentation on 6 September 2026.

## Pillar 1: autonomy, what runs without a human

Base URL `https://app.vaemail.fr/api/v1`, key in the `api-key` header (`X-Api-Key` accepted for compatibility). Only `GET /api/v1/capabilities` works without a key.

Without a human in the loop:

| Step | Call | Scope |
| --- | --- | --- |
| Discover the service, no key | `GET /api/v1/capabilities` | none |
| Check the key and the account state | `npx vaemail init` or `GET /api/v1/usage` | `billing.read` |
| Declare a sending domain | `POST /api/v1/domains` | `domain.write` |
| Get the DNS records to publish | `GET /api/v1/domains/{domain}/dns` | `domain.read` |
| Verify a domain is authenticated | `POST /api/v1/domains/verify` | `domain.write` |
| Know whether a send would go out | `POST /api/v1/messages/validate` | `email.send` |
| Send | `POST /api/v1/transactional/send` with `Idempotency-Key` | `email.send` |
| Track delivery | `GET /api/v1/messages/{id}` | `email.read_status` |
| Diagnose bad placement | `GET /api/v1/deliverability/diagnose?domain=` | `analytics.read` |
| Know what is left | `GET /api/v1/usage` | `billing.read` |
| Report what was done | `GET /api/v1/audit-logs` | `analytics.read` |

The MCP server covers the same ground: 18 tools, local over stdio, zero dependencies. See [mcp-email-server](https://vaemail.fr/mcp-email-server).

## Pillar 2: reliability, accepted is not delivered

**Dry run.** `POST /api/v1/messages/validate` says whether a send would pass, without sending anything and without consuming quota. The response carries `sendable`, a `blocking` list using the same codes as real errors, plus `quota_remaining` and `daily_key_remaining`. A suppressed recipient shows up here, before the send.

**Idempotency.** The `Idempotency-Key` header memorises the response for 24 hours and replays it identically, with `Idempotent-Replay: true`. The same key with a different body is refused with `409 IDEMPOTENCY_KEY_REUSED`, which is a bug in the key derivation, not a replay. Server errors (5xx) are not memorised, so a retry after a 5xx with the same key is safe.

A send answers `202` with `{ "success": true, "data": { "id": 2841, "status": "queued" } }`: accepted and queued, not arrived. Only `GET /api/v1/messages/{id}` (scope `email.read_status`) settles that, through seven states: `queued`, `sent`, `delivered`, `failed`, `suppressed`, `held`, `cancelled`. Poll a few times with a pause of a few seconds. Never announce a delivery from a 202.

```bash
BASE="${VAEMAIL_BASE_URL:-https://app.vaemail.fr}"
api() { curl -sS -X "$1" "$BASE$2" -H "api-key: $VAEMAIL_API_KEY" -H "Content-Type: application/json" "${@:3}"; }

# Derive the key from the business action, so a retry never sends twice.
IDEMPOTENCY_KEY="invoice-2412-${TO}"

# 1. Dry run: nothing is sent.
CHECK=$(api POST /api/v1/messages/validate -d "$(jq -cn --arg to "$TO" --arg s "$SUBJECT" --arg h "$HTML" '{to:$to,subject:$s,html:$h}')")
if [ "$(echo "$CHECK" | jq -r '.data.sendable')" != "true" ]; then
  echo "Blocked:" >&2; echo "$CHECK" | jq '.data.blocking' >&2; exit 1
fi

# 2. Send. 202 means accepted, not delivered.
SENT=$(api POST /api/v1/transactional/send -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d "$(jq -cn --arg to "$TO" --arg s "$SUBJECT" --arg h "$HTML" '{to:$to,subject:$s,html:$h}')")
ID=$(echo "$SENT" | jq -r '.data.id // empty')
if [ -z "$ID" ]; then echo "$SENT" | jq '.error' >&2; exit 1; fi
echo "accepted, id=$ID"

# 3. Delivery status. Only "delivered" means it arrived.
for _ in $(seq 1 10); do
  STATUS=$(api GET "/api/v1/messages/$ID" | jq -r '.data.status')
  echo "status=$STATUS"
  case "$STATUS" in delivered|failed|suppressed|cancelled|held) break ;; esac
  sleep 3
done
```

Full script: `curl/send.sh` in [github.com/vaemail/examples](https://github.com/vaemail/examples).

## Pillar 3: safety, a bounded key cannot overflow

Guardrails sit on the API key and are all optional: a key that declares neither scope nor cap is unrestricted, it holds every scope and no daily cap, which is how keys created before the guardrails existed still behave. Ask a human for a bounded key. For the checklist below, ask for `email.send`, `email.read_status`, `analytics.read` and `billing.read`, plus `domain.read` and `domain.write` if the agent configures the sending domain. Caps: `max_recipients_per_email` 1, `max_emails_per_day` at the expected volume, `allowed_domains` limited to the single sender domain.

Eight scopes: `email.send`, `email.read_status`, `domain.read`, `domain.write`, `contacts.read`, `contacts.write`, `analytics.read`, `billing.read`. A call outside its scope gets a `403 INSUFFICIENT_SCOPE` that names the missing scope and the ones the key holds, so the agent can name what it lacks. Each endpoint states the scope it requires, and the OpenAPI 3.1 spec carries it in `x-vaemail-scope`.

Three caps per key:

- `max_emails_per_day`, the daily send cap. Past it, `429 DAILY_LIMIT_REACHED`, marked `retryable: true` since the cap resets.
- `max_recipients_per_email`. Set it to 1 for transactional mail.
- `allowed_domains`, the sender domains permitted. A send from any other domain gets `403 SENDER_DOMAIN_NOT_ALLOWED`.

`GET /api/v1/usage` (scope `billing.read`) returns the monthly quota, today's sends, and for the current key its scopes, daily cap and remainder. Stop on that number, not on a counter of your own. Caps are expressed in emails, not in money: there is no per-message price in the API.

Keys are stored hashed, shown once at creation, can carry an expiry date and can be revoked. One key per agent, named after it.

## Pillar 4: explainability, a machine-readable account

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

```json
{
  "success": false,
  "message": "The sender domain is not one of those allowed for this key.",
  "error": {
    "code": "SENDER_DOMAIN_NOT_ALLOWED",
    "message": "The sender domain is not one of those allowed for this key.",
    "retryable": false,
    "resolution": { "action": "use_allowed_sender_domain" },
    "documentation": "https://vaemail.fr/docs/errors#sender_domain_not_allowed"
  }
}
```

`retryable: false` means the identical call will never succeed: change something or stop.

`GET /api/v1/audit-logs` (scope `analytics.read`) returns one line per call: which key, which agent, which operation, which parameters, which result, how many emails were engaged. Message bodies, attachments and merge variables never appear. It filters on `operation`, `since`, `limit` and `cursor`. Use it to report what was done rather than reconstruct it from memory.

## What still requires a human

Name these, never declare them done.

- **Opening the account and creating the API key**, or changing its scopes and caps. There is no signup endpoint.
- **Publishing DNS records** at the registrar. The API returns the values, not access to the zone. A record marked `publishable: false` still holds a placeholder that depends on the sending server: publishing it as-is would break authentication. Ask for the value, do not invent one.
- **Sending a campaign to a whole list.** `POST /v3/emailCampaigns/{id}/sendNow` goes out immediately to the entire list. It requires an explicit confirmation in the request body and is marked `x-vaemail-destructive: true` in the spec. That guardrail exists because on 5 September 2026 a call with no body sent 2,982 emails on a Saturday evening. Ask the user in plain words, with the list size, and prefer `sendTest` first.
- **Paying, changing plan, raising a quota.**
- **Removing an address from the suppression list**, and erasing a contact's personal data (`/api/v1/contacts/erase` is irreversible).

## Checklist an agent can follow

1. Confirm `VAEMAIL_API_KEY` is in the environment. Never ask for it in the chat, never write it to a versioned file or a log.
2. On a network drop, retry with the same key. On `409 IDEMPOTENCY_KEY_REUSED`, fix the derivation instead of retrying.
3. On failure, branch on `error.code` and apply `error.resolution.action`. On `RECIPIENT_SUPPRESSED`, stop and report; do not work around the suppression list.
4. On `DAILY_LIMIT_REACHED` or `QUOTA_EXCEEDED`, stop and tell the human how many went out and how many remain.

## Copy this to your agent

The ready-to-paste prompt lives on the [agents page](https://vaemail.fr/agents): base URL, environment variable, validate-before-send, idempotency, delivery status and the human-only steps, in one block.

## Limits worth knowing before you build on it

No inbox is created on the fly, so an agent cannot get an address to receive a verification code or a human reply; inbound exists only for prospecting sequences attached to a declared IMAP mailbox. There is a Python SDK (`pip install vaemail`), and Brevo clients work against `/v3`. The npm MCP server runs on stdio. A separate remote connector at `mcp.vaemail.fr`, with OAuth, exposes 16 tools. Six webhook events: sent, delivered, bounce, complaint, opened, clicked, with no signature documented, against 15 to 19 events elsewhere. The documentation is partly in French. The product is young, run by an agency. Servers are in Germany, sending in Europe. Prices are public on https://vaemail.fr/tarifs/.

## Links

- Agent page: https://vaemail.fr/agents
- Security and guardrails: https://vaemail.fr/docs/security.md
- Agents documentation: https://vaemail.fr/docs/agents.md
- Error codes: https://vaemail.fr/docs/errors.md
- API reference: https://vaemail.fr/docs/api.md
- Everything 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
- Agent skill: https://github.com/vaemail/skills
- Comparison with Resend, Brevo, Postmark, AgentMail: https://vaemail.fr/compare/index.md
- Related pages: [email-api-for-ai-agents](https://vaemail.fr/email-api-for-ai-agents), [mcp-email-server](https://vaemail.fr/mcp-email-server), [email-for-claude-code](https://vaemail.fr/email-for-claude-code), [ai-agent-email-deliverability](https://vaemail.fr/ai-agent-email-deliverability)
