Skip to content

v1.5.0

Latest

Choose a tag to compare

@jiashuoz jiashuoz released this 02 Aug 03:50
31b2c3b

/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_request across the /v1 surface,
    including text, html, and attachment filenames on the send path (#780).
    Previously a NUL in a message body was accepted with 202 and a NUL in
    subject, contact display_name, or metadata surfaced as a 500. 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, returns 400 invalid_cursor. This closes a real bug — a foreign
    cursor previously anchored a query to a meaningless position and returned a
    silently truncated or empty 200. 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 whose display_name or address exceeds its documented limit now
    fails individually: the request returns 200 with that row marked failed and
    its neighbours imported, where 1.4.1 returned a whole-request 422. Clients
    that treated a 2xx as "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
    use ETag/If-Match.
  • Scheduled sending (#665, beta) — send_at on send, reply, and forward.
    A future schedule returns status=scheduled with scheduled_at as 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_id on message reads, a
    mailbox-local topology identifier derived from In-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 /v1 spec now documents the SMTP octet
    limits on recipient addresses, attachment-filename constraints, and the
    contact.due event 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 dead ETag, 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 with 554 ... 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: undefined when no ETag is
    supplied (#774), which produced spurious 412s. 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.list accepts limit in both.
  • MCP tool count 71 → 76. delete_suppression and delete_agent_suppression
    require confirm: 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.