# AI agent email deliverability

Mail sent by an agent lands badly for two documented reasons: the sending domain is not authenticated, or the recipient list has worn out. A third failure is specific to programs: a retry without `Idempotency-Key` sends the message a second time. The domains endpoints report the live state of SPF, DKIM and DMARC, `POST /api/v1/messages/validate` tests a send without sending it, and `GET /api/v1/deliverability/diagnose` returns a verdict with the issues found and the actions that fix them. Publishing the DNS records stays a human step, and the API is explicit about which values must not be published yet.

Facts checked against the VaEmail documentation on 6 September 2026.

## What an agent gets wrong that a person does not

An agent does not open a dashboard, so an unauthenticated domain goes unnoticed: nothing in the send path fails, the mail simply lands in junk. Mail from a domain without SPF and DMARC is classified as junk regardless of content: major providers check both before deciding where to put a message.

List wear is the second cause. Addresses that no longer exist push the hard bounce rate up, and a high rate damages the domain's reputation for everything sent afterwards, transactional mail included. The documentation separates two things that get confused: a hard bounce is list wear, the address is gone and already blocked, while a complaint questions the content or the origin of the contacts and is the heavier signal for a provider.

Third, retries. A program that receives a timeout sends again. Without `Idempotency-Key`, that is a second real message; the header memorises the response for 24 hours and replays it instead. The replay carries `Idempotent-Replay: true`. The same key reused with a different body is refused with 409 `IDEMPOTENCY_KEY_REUSED`: that is a key derivation bug, not a replay. Server errors (5xx) are not memorised, so retrying them with the same key is safe.

## Step 1: authenticate the sending domain

```
POST /api/v1/domains
{ "domain": "example.com" }
```

Scope `domain.write`. Redeclaring the same domain returns its state instead of creating a duplicate. The response carries the current authentication state and the records to publish:

```json
{
  "domain": "example.com",
  "authentication": { "spf": {...}, "dkim": {...}, "dmarc": {...}, "ok": false },
  "dns_records": [
    { "type": "TXT", "host": "@", "value": "v=spf1 ...",
      "role": "SPF, autorise notre serveur à envoyer pour ce domaine.",
      "publishable": true, "missing_values": [] }
  ],
  "dns_ready": true,
  "dns_note": null,
  "next_step": "Poser les enregistrements DNS, puis rappeler POST /api/v1/domains/verify."
}
```

`GET /api/v1/domains/{domain}/dns` (scope `domain.read`, MCP tool `vaemail_dns_requirements`) returns the same records for a domain already declared. Propagation takes minutes to hours. Then `POST /api/v1/domains/verify` (scope `domain.write`, MCP tool `vaemail_verify_domain`) reads the public DNS live, not a state stored on the VaEmail side, and returns a verdict per record: `spf`, `dkim`, `dmarc`, plus `ok`.

The records answer different questions. SPF names the servers allowed to send for the domain, which is what separates your send from a forgery. DKIM signs each message, and the signature survives transit. DMARC says what to do when both fail, and where reports go; start at `p=none`. `ok` becomes true as soon as SPF and DMARC are present, since DKIM depends on the selector and is only checked when one has been declared.

## The publishable: false rule

Some values depend on the sending server's configuration and are not known when the response is built. The record then carries `publishable: false`, and `missing_values` names what is still needed. `dns_ready` summarises the whole set, `dns_note` says what is missing.

Publishing a record in that state breaks the domain's authentication silently. No error appears anywhere: the domain looks configured and the mail goes out badly. An agent surfaces the record, names the missing value and stops. It does not invent a placeholder value, and it does not report the domain as configured.

## Step 2: validate before sending

```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@example.com","html":"<p>Bonjour</p>"}'
```

Nothing is sent, nothing is consumed. The response carries `sendable`, `blocking` (each item with the same `code` as the matching real error), `warnings`, `quota_remaining` and `daily_key_remaining`. A suppressed recipient and an unverified domain both show up there. Scope `email.send`, MCP tool `vaemail_validate_email`.

## Step 3: read the diagnosis

```bash
curl "https://app.vaemail.fr/api/v1/deliverability/diagnose?domain=example.com" \
  -H "api-key: $VAEMAIL_API_KEY"
```

Scope `analytics.read`. In MCP: `vaemail_diagnose_deliverability`. On the command line: `vaemail doctor`. The `domain` parameter is optional; an undeclared domain returns 404. The OpenAPI schema documents four fields in `data`:

- `status`, one of `ok`, `warning`, `critical`. A verdict readable without parsing the rest.
- `authentication`, one verdict per declared domain, read from the public DNS at call time.
- `issues`, each with `key`, `level` (`info`, `warning`, `critical`), `title`, `summary`, `cause` and `actions`, ordered most serious first.
- `recommended_actions`, what to do, each carrying the endpoint that performs it.

The deliverability page also describes `by_domain`, delivery rates per recipient domain, which is how you spot one provider filtering while the rest deliver. Those statistics have their own endpoint too, `GET /api/v1/stats/deliverability`, same scope.

## Bounces, complaints and the suppression list

```
GET /api/v1/suppressions
```

Scope `contacts.read`, MCP tool `vaemail_list_bounces`. Every address listed there is refused at send time with `RECIPIENT_SUPPRESSED`. A program that never reads this list retries dead addresses forever and raises its own bounce rate. Filters: `reason` (`hard_bounce`, `soft_bounce_limit`, `complaint`, `unsubscribe`, `manual`), `since`, `limit` up to 200, `cursor`. Removing an address is a human decision, and `retryable` is false, so the identical send will not start working later.

## The seven delivery states

A send returns 202 with `{"data": {"id": 2841, "status": "queued"}}`. Accepted, not delivered. The outcome is read on `GET /api/v1/messages/{id}` (scope `email.read_status`), where `status` is one of `queued`, `sent`, `delivered`, `failed`, `suppressed`, `held`, `cancelled`. `error` carries the transport's reason, `events` the sequence of what happened, each with `type`, `classification` and `at`. Only `delivered` lets an agent say the mail arrived; the published curl example polls with a pause and stops on any terminal state.

Webhooks cover six events: sent, delivered, bounce, complaint, opened, clicked.

## What stays human

Creating the account and the API key: there is no signup endpoint. Publishing DNS records at the registrar: the API returns values, not access to the zone. Paying, changing plan or raising a quota. Removing an address from the suppression list. Sending a campaign to a whole list, which requires explicit confirmation in the request body. Name these steps, never declare them done.

## Copy this to your agent

```
Diagnose why email from this project is landing badly, using VaEmail.
Documentation for agents: https://vaemail.fr/llms.txt
OpenAPI specification: https://app.vaemail.fr/openapi.json

Rules:
- The API key is in the environment variable VAEMAIL_API_KEY. Never write it to a
  versioned file.
- Start with GET /api/v1/deliverability/diagnose. Report status, the issues in order and
  the recommended actions. Do not guess a cause it did not return.
- For each unauthenticated domain, call GET /api/v1/domains/{domain}/dns and give me the
  records. A record with publishable false is missing a value: name it and stop.
- Publishing DNS records is my job. Never report a domain as configured on the strength of
  records you produced.
- 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.
- A 202 means accepted, not delivered: read GET /api/v1/messages/{id}, only "delivered"
  counts as arrived.
- Read GET /api/v1/suppressions before retrying any address.
```

## Limits worth knowing

VaEmail creates no inbox on demand, so an agent cannot obtain an address to receive a verification code or a 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, 18 tools. A separate remote connector at `mcp.vaemail.fr`, with OAuth, exposes 16 tools. Webhooks are signed with HMAC-SHA256: https://vaemail.fr/docs/webhooks.md. Part of the documentation is in French, and the product is young, run by an agency. Public price grid: https://vaemail.fr/tarifs/.

One caveat about the subject itself: inbox placement depends first on the sending domain and on what is being sent, not on the provider. None of the five providers compared publishes an independent placement measurement.

## Links

- Deliverability: https://vaemail.fr/docs/deliverability.md · Domains: https://vaemail.fr/docs/domains.md
- Start here: https://vaemail.fr/docs/quickstart.md · API reference: https://vaemail.fr/docs/api.md
- Error codes: https://vaemail.fr/docs/errors.md · Scopes and caps: https://vaemail.fr/docs/security.md
- MCP server and CLI: https://vaemail.fr/docs/mcp.md · Source: https://github.com/vaemail/vaemail-mcp
- OpenAPI 3.1: https://app.vaemail.fr/openapi.json · Capabilities, no key: https://app.vaemail.fr/api/v1/capabilities
- Everything in one file: https://vaemail.fr/llms-full.txt · Examples: https://github.com/vaemail/examples
- Related: https://vaemail.fr/email-api-for-ai-agents · https://vaemail.fr/autonomous-agent-email · https://vaemail.fr/mcp-email-server · https://vaemail.fr/email-for-claude-code
