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.

api serve reports that address before it can report an outcome, so it is an ordered event stream: every event goes to stdout, and --output-to split is refused. Read one stream and branch on kind.

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 the same LAN route probe as review --mode link. The probe targets the IPv4 all-hosts multicast route rather than the default unicast route, so a VPN or overlay such as Tailscale can own the default route without displacing the physical LAN address afmail advertises. LAN mode 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.