/v1 is now generally available. This is the release that completes the API
freeze: the core /v1 surface is stable and will not take breaking changes, and
the surface that is still moving is explicitly enumerated below and marked in the
OpenAPI spec. Earlier v1.0.x application tags predate the freeze and are not
/v1 compatibility baselines.
Backward compatible with 1.4.1 on the wire — the previous release's full
conformance suite passes unmodified against this build (268 assertions), and its
published SDKs and CLI work against this server. Three behavior changes are
deliberate and are called out under "Behavior changes" below; read that section
before upgrading. Fourteen migrations, six of which build indexes
CONCURRENTLY on messages — see "Upgrading".
Behavior changes
Each of these changes an outcome that 1.4.1 produced. None removes an operation,
field, or enum value.
-
NUL bytes are rejected with
400 invalid_requestacross the/v1surface,
includingtext,html, and attachment filenames on the send path (#780).
Previously a NUL in a message body was accepted with202and a NUL in
subject, contactdisplay_name, ormetadatasurfaced as a500. If you
relay third-party content, encode or strip control characters before sending. -
All existing pagination cursors are invalidated (#782). Cursors are now
signed and bound to the collection and the account that minted them; a cursor
from before this release, or one replayed against a different endpoint or
account, returns400 invalid_cursor. This closes a real bug — a foreign
cursor previously anchored a query to a meaningless position and returned a
silently truncated or empty200. If you persist cursors for
checkpoint/resume, they will not survive the upgrade; restart the affected
queries. Cursors remain opaque, and a durable identifier is still the right
thing to checkpoint on. -
Contact import reports per-row failures instead of rejecting the batch
(#780). A row whosedisplay_nameor address exceeds its documented limit now
fails individually: the request returns200with that row marked failed and
its neighbours imported, where 1.4.1 returned a whole-request422. Clients
that treated a2xxas "every row imported" must read the per-row results.
API
- Contacts and outreach (#754, beta) — account-level contact identity, CSV
import with per-row outcomes and reversible batches, and per-agent outreach
state with stage, scheduling, and suppression visibility. Conditional updates
useETag/If-Match. - Scheduled sending (#665, beta) —
send_aton send, reply, and forward.
A future schedule returnsstatus=scheduledwithscheduled_atas a
not-before bound. Trashing before the fire time cancels; restoring before it
re-arms. At most 90 days ahead; an explicit UTC offset is required. - Thread identity (#765, beta) —
thread_idon message reads, a
mailbox-local topology identifier derived fromIn-Reply-To/References.
Deliberately mailbox-local: two correspondents' mailboxes mint different ids
for the same exchange. - Suppressions across every client surface (#791) — account-wide and
agent-scoped suppression management now reachable from the dashboard, the CLI
(e2a suppressions), and MCP (five new tools). Account-wide suppressions are
GA and read-only apart from removal; entries arrive only from real bounce and
complaint feedback. Agent-scoped management is beta. - Per-agent fire-time send rate limiting (#770) — a durable 60/min/agent
sliding window enforced immediately before provider submission. A deferral
snoozes the job without burning an attempt or emitting a terminal event. - Contract precision (#784) — the
/v1spec now documents the SMTP octet
limits on recipient addresses, attachment-filename constraints, and the
contact.dueevent payload that previously had no schema.
Correctness
- Post-write reads return authoritative state (#783, #775). Conditional
engagement updates returned the pre-update row and a deadETag, a Postgres
data-modifying-CTE snapshot hazard; write paths now return the committed row. - Pagination cursors are bound to their minting endpoint and account (#782)
— see "Behavior changes". - The limits cache can no longer be grown without bound (#781). The
invalidation path is bounded by a process-wide epoch rather than retaining
per-user state for the process lifetime. - Long inbound lines no longer bounce (#773). The inbound relay accepted only
2000-byte lines, so agent-generated mail carrying single-line JSON or unfolded
HTML was rejected at DATA with554 ... too long a line in input stream. The
limit is now 128 KiB, and rejections are recorded as
e2a_smtp_inbound_total{outcome="rejected_line_too_long"}instead of being
invisible. - Agent-enumeration oracles closed on the consent and dashboard paths (#766).
- TypeScript SDK no longer sends
If-Match: undefinedwhen no ETag is
supplied (#774), which produced spurious412s. Fixed in 5.5.0; the CLI and
MCP server inherit it. The Python SDK was unaffected.
Clients
- TypeScript SDK 5.5.0, Python SDK 5.5.0, CLI 2.2.0, plugin 0.6.0.
- Both SDKs gain contacts, outreach, scheduled sending, and suppressions;
account.suppressions.listacceptslimitin both. - MCP tool count 71 → 76.
delete_suppressionanddelete_agent_suppression
requireconfirm: true, so an agent cannot restore sendability or drop an
unsubscribe block on ambiguous context.
Stability
The GA/beta split is documented in
docs/api.md → Stability and marked
machine-readably in the spec: x-stability-level: beta on beta operations,
schemas, and fields, and x-experimental-values where only specific values of an
otherwise-stable field are beta.
Beta today: contacts and outreach, scheduled sending, email templates and starter
templates, the reviews (HITL) queue, agent protection config, agent-scoped
suppression management, managed unsubscribe, message lifecycle diagnostics, and
thread_id. Everything else in /v1 is covered by the freeze.
Upgrading
Fourteen migrations. Six build or drop indexes on messages CONCURRENTLY
(087–090, 092, 093). CONCURRENTLY does not lock writes, but on a large
messages table these can take minutes and they run before the server reports
ready — on a deployment with a readiness or health-check budget, apply them
ahead of the rollout rather than letting the deploy carry them.
No configuration changes are required.