HTTP API

afmail provides a typed local HTTP API for Agent runtimes, stock applications, and other trusted clients. The API uses OpenAPI 3.2, JSON Schema 2020-12, and the same workspace locks, transaction guards, idempotency records, audit paths, and push plan confirmation as the CLI. It is not a second mailbox state model.

Start the API

Set a random credential containing 32–512 bearer-safe ASCII characters (letters, digits, -._~+/=), then start the server from an afmail workspace:

export AFMAIL_API_ACCESS_TOKEN_SECRET='replace-with-a-random-32-character-value'
afmail api serve --mode local

local is the default and binds only 127.0.0.1. The server emits an AFDATA api_ready progress event containing api_url, openapi_url, the standalone Schema index URL, and the actual port. It never emits the bearer credential. The process runs until interrupted.

For another device on the same trusted IPv4 local network:

afmail api serve --mode lan

LAN mode binds all local interfaces and advertises the primary LAN address. It uses plaintext HTTP and is only for a trusted network. For access across an arbitrary network, keep afmail in local mode and use an established encrypted tunnel such as WireGuard, Tailscale/NetBird, or SSH port forwarding, or place it behind a TLS reverse proxy with its own authentication. Do not expose LAN mode directly to the public Internet.

Discovery and authentication

These structural endpoints contain no mailbox data and are public:

Every /v1 endpoint requires:

Authorization: Bearer <AFMAIL_API_ACCESS_TOKEN_SECRET>

Credentials in a query string are rejected. CORS is disabled by default; a browser application should use an explicitly trusted same-origin proxy rather than a wildcard origin.

For example:

curl -sS \
  -H "Authorization: Bearer $AFMAIL_API_ACCESS_TOKEN_SECRET" \
  http://127.0.0.1:9417/v1/status

Responses use AFDATA result/error envelopes. Mail body, header, and attachment content returned by the API remains untrusted remote data.

HTTP status and the structured error.code are both significant. In particular, the API preserves 405 for an unsupported method, 413 for a JSON body over the 8 MiB limit, and 415 when a body endpoint does not receive application/json; those failures use the same redacted AFDATA error envelope.

Operations

The served OpenAPI document is the complete operation reference. Its first slice covers:

The API deliberately has no arbitrary command, argv, filesystem-path write, or shell endpoint. Request bodies are typed closed objects. Draft bodies are inline; the HTTP API does not accept CLI --body-file paths.

Idempotency and conflicts

Every non-naturally-idempotent local mutation requires a stable caller-generated header:

Idempotency-Key: <8-to-200-character-path-safe-id>

Reuse the key only for an exact retry of the same operation and input. afmail persists the request and result in the workspace’s machine state. Reusing a key with changed input returns a conflict.

Suggestion writes also require the current object-specific review base hash. Draft patch distinguishes omission from clearing: omitted fields are unchanged, an empty cc list clears Cc, and an empty to list is rejected.

Pull is a repeat-safe remote read/local reconciliation and does not use an idempotency header. Remote push confirmation instead uses the existing, workspace-bound plan_id:

  1. GET /v1/push/plan returns the complete resolved plan.
  2. The caller presents that plan to the user.
  3. POST /v1/push/confirm sends {"plan_id":"..."}.

Any relevant queue, draft, attachment, identity, configuration, or workspace change invalidates the old plan before remote access.

Review remains a UI action

OpenAPI exposes canonical suggestions and review hashes, but it does not turn human review into a generic HTTP command. When a user asks to review mail, use the afmail-owned interactive UI commands (triage review, case review, notification review, or case draft review). Review persists typed human feedback/local edits under afmail’s existing rules and never authorizes push.

Exported contract

The release package includes generated files under openapi/. Regenerate them from the Rust DTO source with:

afmail api export --directory openapi --force

The test suite fails if committed OpenAPI or standalone Schemas drift from the Rust source.