# Mailery API Changelog

All notable changes to the Mailery API contract (`/api/v1`) and public web
surface are recorded here. See the
[Versioning And Deprecation](./API.md#versioning-and-deprecation) policy for
how breaking changes and deprecations are announced.

## Unreleased

- **Repeatable `?domain=` mailbox-scope filter** on `GET /api/v1/messages` and
  `GET /api/v1/messages/groups` (shared self-hosted/cloud contract): scope the
  list/counts to the tenant's mailboxes whose **address** is at one of the
  given domains (e.g. `?domain=corp-a.test&domain=corp-b.test`).
  Case-insensitive, tolerates a leading `@`, max 16 distinct values; IDN
  domains match under either spelling (unicode or punycode); a blank or
  whitespace-only value collapses to no filter (like `?folder=`), a malformed
  value is `400`, and combining with `updatedSince` is `400`. Index-backed at
  scale: the filter resolves to mailbox ids via a new expression index
  (migration 0014) and runs one bounded keyset descent per mailbox
  (`unnest` + `LATERAL` top-N merge, O(mailboxes × page)) — never an offset
  scan, a full-domain sort, or a full-tenant walk. Counts stay on the
  precomputed `message_counters` rows (no live `COUNT(*)`).
- `GET /api/v1/messages/:id` and `GET /api/v1/messages/:id/attachments` no
  longer read inline attachment bodies from the database just to discard them
  at serialization: the envelope attachment projection is metadata-only
  (`content_base64` was already never on the wire; `body.available` is now
  derived from a presence flag). Cuts multi-MB detoasts per detail read for
  postgres-stored attachments (~20ms → ~0.3ms with 2×3MB attachments).
- **Breaking operator executable boundary:** the private `platform-mailery`
  client is now remote API-only. Its former local `doctor`, `migrate`, and
  `ses-worker` commands moved to the explicitly deployment-scoped
  `platform-mailery-runtime` executable. Production compose/ECS and repository
  migration workflows use that runtime surface. The existing
  `platform-mailery-migrate` bin remains as a runtime-scoped compatibility
  alias. The public `mailery` CLI no longer inspects or deletes historical
  local config files during client construction or command startup.
- Subscription qualifying-credit provenance is now isolated per paid invoice
  grant, rather than pooled per subscription. Payment refunds, failed-operation
  refunds, and invalid-plan quarantine therefore cannot reuse a later invoice's
  entitlement capacity or affect another subscription/credit-pack origin.
- **The Free tier is live (PIVOT D-4).** A fresh verified tenant now mints API
  keys (`POST /api/v1/api-keys`) **without a subscription or credits** —
  `402 billing_required` moved to the PAID surface (the custom-domain routes)
  and its `details` now carry an upgrade `hint`. The free plan is 1 mailbox on
  the shared subdomain + 3,000 sends per UTC calendar month:
  - `GET /api/v1` exposes the deployment's shared free-tier domain as
    `shared_domain` (`MAILERY_SHARED_DOMAIN`; `null` when unconfigured).
  - `POST /api/v1/mailboxes`: free-plan tenants must place their single
    mailbox on the shared subdomain (`402 free_plan_shared_domain_required`;
    `503 shared_domain_not_configured` when the server has no shared subdomain).
    Shared-domain addresses are unique across ALL tenants
    (`409 mailbox_address_taken`) and always use Mailery's hosted mail plane.
  - `POST /api/v1/messages/send`: sends from the shared subdomain skip the
    per-tenant domain-readiness gates; free-plan sends draw from the monthly
    allowance first, then any purchased credit balance, and return
    `402 send_limit_reached` (with an upgrade `hint`) at the cap. The
    allowance never rolls over.
  - Message operations (upload, bulk, flags, labels, delete) and the
    credit-metered Groq features (parse, digest) pass the billing gate for
    free-plan tenants; parse/digest still require credits
    (`402 insufficient_credits` at 0 balance).
- `POST /api/v1/digests/generate` is now idempotent per (tenant, window, UTC
  day) end-to-end: a same-day repeat returns the stored digest without
  re-running the model or re-billing (the credit charge was already
  day-idempotent), and the route is rate-limited per tenant (60/hour, `429`).
- Added product feedback: `POST /api/v1/feedback` (submit) and
  `GET /api/v1/feedback` (list your own). Any authenticated tenant actor (user
  session or API key) may use it — deliberately **not** billing-gated. Submissions
  are persisted tenant-scoped in Postgres via the `@hasna/feedback` SDK store
  (new `feedback` table + `tenant_isolation` RLS policy). Rate-limited to 30 per
  tenant and 60 per client IP per hour. Accepts an optional message `context`
  and a best-effort client PNG `screenshot` (decoded, 6 MB size-capped, stored
  by reference — never embedded in the row; a bad image never blocks the text).
  Surfaced through the CLI feedback command and the authenticated API.
- Unknown non-API paths now return HTTP `404` (previously they served the
  landing shell with `200`). Unknown `/api/*` and `/webhooks/*` paths keep the JSON
  `not_found` error shape. Operators: point uptime monitors and bookmarks at
  `/`, `/docs`, or `/health` — arbitrary paths such as `/favicon.ico` or
  `/index.html` no longer return `200`.
- Retired app routes issue permanent `308` redirects to `/`, preserving the
  query string for legacy checkout returns and emailed reset/verify links. All
  other unknown paths remain honest `404`s.
- `402 billing_required` error details now include a `past_due_recovery`
  object when the tenant's newest subscription is `past_due`, naming the two
  recovery paths: fix payment via `POST /api/v1/billing/portal`, or cancel and
  continue spending a qualifying credit balance. `past_due` itself remains
  non-entitling; a positive qualifying credit balance keeps access until it
  reaches `0`.
- `POST /api/v1/api-keys` now enforces the verified-email requirement directly
  at the mint route (previously enforced only transitively through the billing
  gate). No observable change under production defaults; credit-entitled
  (including trusted-signup) tenants are now explicitly covered.
- `POST /api/v1/auth/password/forgot` returns its uniform `200` immediately;
  token issuance and email delivery run asynchronously after the response, so
  response timing no longer distinguishes real accounts from unknown emails.
  Body, status, and rate limits are unchanged.
- Stripe checkout returns land on `/?checkout=success|cancel`; owner billing
  state is available through `GET /api/v1/billing/overview` and the CLI billing
  command.
- Docs: published per-endpoint rate limits, credit-pack pricing, and the API
  versioning/deprecation policy in `docs/API.md`.

## 0.1.x

- Initial public `v1` surface: auth and sessions, scoped API keys, billing
  (credits, subscription, Stripe checkout/portal/webhook), mailboxes,
  messages (list/search/delta/bulk/labels/tombstones), attachments, digests,
  tenant domain lifecycle, provider webhooks, and platform admin routes.
