Product release notes

Every Sendry ship, in one place

Follow new features, UX polish, and fixes as they land. The latest release is highlighted first, with the full timeline below.

Releases tracked

27

Newest first from the public changelog

Updates shipped

189

Features, polish, and fixes across versions

Latest version

v2.10.3

June 20, 2026

Added

120

Net-new capabilities shipped

Improved

10

UX, workflows, and platform polish

Fixed

59

Bugs and reliability fixes

Latest releasev2.10.3June 20, 2026

Freshly shipped updates

1 update in this release, covering 1 fix.

Added0
Improved0
Fixed1
  • Fixed

    Changelog page only showed one release

    the release timeline used framer-motion whileInView with an initial opacity of 0, so every entry rendered invisible in SSR and only the in-viewport card faded in; the rest stayed hidden until scrolled to (and stayed hidden entirely for no-JS/crawlers), making the page look like it had a single entry. The timeline articles and the summary card now animate in on mount (animate instead of whileInView), so all releases are visible immediately. Data was never affected — /v1/changelog already returned all 26 releases.

v2.10.2June 20, 2026

Release highlights

4 updates in this release, covering 4 fixes.

Added0
Improved0
Fixed4
  • Fixed

    Dashboard error boundaries (UX-H9)

    the only error boundary in the app was the bare, unstyled root one, so any uncaught render/loader error showed a white error page. Added a styled root boundary (badge + message + Reload / Go home, dev stack trace) and a dashboard-scoped ErrorBoundary on routes/dashboard/_layout.tsx (Reload / Go to Overview) so a crashing dashboard route degrades gracefully instead of nuking the shell.

  • Fixed

    List pages tell "couldn't load" apart from "empty" (UX-H8)

    campaigns, API keys, domains, webhooks, settings, and contacts previously rendered the normal empty state ("No campaigns", "No API keys"…) when the list fetch failed, making users think their data was deleted. Each now renders a distinct "Couldn't load … — Retry" branch (with a toast) when the first load errors and there's nothing to show; a failed background revalidation no longer blows away rows already on screen.

  • Fixed

    Public status page survives an API outage (UX-H5 / PERF-L9)

    the status loader threw on fetch failure, bubbling to the unstyled root boundary exactly when the page is needed most. It now uses a 2s fetch timeout and renders a styled "temporarily unavailable" state inside the normal shell. Added real useRevalidator auto-refresh (30s) to match the "checks periodically" copy, with the relative-timestamp computed client-side to avoid a hydration mismatch (UX-M6).

  • Fixed

    Marketing credibility fixes

    billing copy now says Paystack (was Stripe) on About + Features (UX-H3); footer GitHub points at the real sendry-dev org and the placeholder Twitter link was removed (UX-H2); near-invisible light-mode cyan on the Features bento icons and the pricing page now use text-cyan-600 dark:text-cyan-300 (UX-H4 / UX-M13); fixed a broken quick-start code sample (apostrophe in a single-quoted string, UX-M1); reworded the self-contradicting billing-period FAQ answer (UX-M2); relabeled the non-existent "Start Free Trial" CTA to "Get Started" (UX-M3); corrected the SDK count on About and the garbled broadcast plan-availability copy (UX-M4/M5); fixed a landing code-sample caption (UX-L4).

v2.10.1June 20, 2026

Release highlights

1 update in this release, covering 1 improvement.

Added0
Improved1
Fixed0
  • Improved

    Dashboard SWR cache rolled out across the app (PERF-H7 complete)

    the stale-while-revalidate cache shipped in 2.10.0 (overview/emails/contacts/analytics) is now wired into ~49 more dashboard routes: all of contacts, deliverability, sending, settings, and admin/superadmin lists + detail pages, plus audit-log, validation, and migrate. Navigating back to a visited page now shows cached data instantly and revalidates in the background instead of spinner-on-every-mount; mutations call invalidateCache so lists refresh. Sidebar nav links get prefetch="intent" so route modules/data prefetch on hover. Intentionally left on their existing pattern: the stateful editors (compose, visual-editor, template-edit), live/SSE + polling views (api-logs Live tab, superadmin-system, migrate job poll), and debounced-search lists (contacts, unsubscribes, superadmin orgs/users) where SWR's fire-on-key-change would break debounce/abort. Server-loader-backed routes (billing, notifications) need no change. Also fixed a pre-existing conditional-hook order bug in analytics-benchmarks.tsx.

v2.10.0June 20, 2026

Release highlights

53 updates in this release, covering 29 additions, 3 improvements, 21 fixes.

Added29
Improved3
Fixed21
  • Added

    Custom-domain inbound auto-provisioning

    when a domain verifies, Sendry now appends it to a SES receipt rule's Recipients so SES accepts inbound mail for it (and removes it on domain deletion), closing the manual-AWS-step follow-up from the inbound feature. Managed via the classic SES API (@aws-sdk/client-ses; SESv2 has no receipt-rule ops) in src/lib/ses/inbound-provisioning.ts. Read-modify-write on the shared rule is serialized with a Redis lock; both hooks are fire-and-forget and never block verify/delete. Safe by default: a no-op unless both SES_INBOUND_RULE_SET and SES_INBOUND_RULE_NAME are set — the live rule set is never touched otherwise. Guards refuse to modify a catch-all rule or remove the last recipient. New ops script scripts/provision-inbound-domain.ts (--discover / --list / --reconcile [--dry-run] / --add / --remove) discovers the active rule set, backfills, and inspects the rule. Adds dependency @aws-sdk/client-ses.

  • Added

    Custom inbound email addresses

    orgs can now pick a readable handle for their shared-domain inbound address instead of the org_-derived hex default. PUT /v1/inbound/config accepts an optional slug (validated in src/modules/inbound/slug.ts: lowercase, [a-z0-9-], 3–63 chars, no leading/trailing/double hyphen, reserved role-handles blocked; uniqueness enforced with a 409 slug_taken backstop). GET /v1/inbound/config now also returns inbound_domain, slug, and a custom_inbound[] array — one entry per verified domain with the exact MX record (inbound-smtp.<SES_INBOUND_REGION>.amazonaws.com, priority 10) the customer must publish to receive *@their-domain mail, which the app already routes via domains.name. Dashboard inbound Configure dialog gets an inline slug editor (with the @domain suffix) and a per-domain MX card with copy buttons. New env: INBOUND_DOMAIN (default recv.sendry.online, was hardcoded) and SES_INBOUND_REGION (default us-east-1). Design + the build-vs-buy analysis for IMAP/Outlook mailbox support are documented in docs/features/inbound-email.md. GET /v1/inbound/config now returns the full signing secret (was truncated to 8 chars, which broke the dashboard reveal/copy — the customer could never retrieve a working HMAC key; consistent with the webhooks module, gated by full_access) and a slug_is_default flag so the dashboard prompts for a friendly handle instead of prefilling the auto-generated org-id hex.

  • Added

    A/B testing on campaigns

    Send subject + content variants to a configurable split of your audience, optionally hold back a sample for winner rollout, and auto-pick the winner by open or click rate after N minutes. New columns on campaigns (ab_test_*) and campaign_recipients (ab_variant, holdback); new ab-test-evaluator worker on a 5-minute tick; new endpoints GET /v1/campaigns/:id/ab-test/results and POST /v1/campaigns/:id/ab-test/pick-winner. Dashboard campaign create/edit gets an A/B Test section; the detail page shows variant-vs-variant cards with winner badge. Migration 0059. Closes the broadcast A/B-testing gap in docs/COMPETITOR_ANALYSIS.md §5.

  • Added

    Real-time API logs viewer

    Dashboard → API Logs now has a Live tab that streams new API requests as they happen via SSE (/v1/logs/api-requests/stream). Backed by Redis pub/sub; the api-log-writer worker publishes each persisted row to channel api-logs:<orgId> after insert. Stream emits one data: line per row plus a :heartbeat comment every 15s to keep proxies from idling out. Dashboard caps the in-memory buffer at 200 rows and exposes a Pause toggle. Closes the "no real-time logs viewer" gap in docs/COMPETITOR_ANALYSIS.md.

  • Added

    Per-org audit log

    every mutating action on api keys, domains, webhooks, team membership, and organization settings now writes one row to a new audit_log_entries table, surfaced through a filterable dashboard view at /audit-log and the /v1/audit-log API.

  • Added

    Resend migration importer

    One-click flow under Dashboard → Migrate that scans a Resend account via API key and stages domains, audiences, and contacts for import into Sendry.

  • Added

    Email validation API

    Validate addresses for deliverability before sending. POST /v1/validation/email and /v1/validation/batch return syntax, MX, disposable, role-account, free-provider, and did-you-mean signals; results cache for 24h per org.

  • Added

    Paystack subscription billing

    second payment provider alongside Stripe, for the markets Paystack serves (NGN/GHS/USD/ZAR/KES). New module src/modules/paystack/ exposes POST /v1/billing/paystack/checkout (initializes a Paystack transaction, returns the hosted checkout URL), POST /v1/billing/paystack/verify (synchronous fallback to the webhook), and POST /v1/billing/paystack/cancel (disables the subscription via the Subscription API; the downgrade lands when Paystack fires subscription.disable). Recurring subscriptions are created automatically by passing the Paystack Plan code on transaction init — scripts/setup-paystack-plans.ts idempotently creates one Plan per tier/period and prints the PAYSTACK_PLAN_* env lines. The webhook at POST /internal/paystack/webhook is registered public (before global apiKeyAuth) and verified with HMAC-SHA512 of the raw body using the secret key (x-paystack-signature, timingSafeEqual); it handles charge.success, subscription.create, invoice.create/invoice.update, invoice.payment_failed, and subscription.disable/subscription.not_renew, mirroring the Stripe upgrade / overage-reset / SEC-07 suspend / downgrade flows. Every transaction is traceable back to Sendry: the reference is sendry-<orgId>-<random> (Paystack-charset-safe via paystackReference() in src/lib/id.ts) and metadata carries orgId plus labelled custom_fields (Sendry Org / Sendry Plan) that render on the Paystack dashboard. Webhooks resolve the org by metadata orgId (charge) or paystack_customer_code (subscription/invoice). Thin fetch client src/lib/billing/paystack.ts (no SDK dependency) holds a plan-code map mirroring Stripe's price map. Migration 0055_paystack_billing.sql (+ idempotent patch in scripts/apply-manual-migrations.ts) adds payment_provider and paystack_customer_code / paystack_subscription_code / paystack_email_token / paystack_current_period_end to organizations (all nullable; existing Stripe/free orgs unaffected) and indexes the customer code. New optional env: PAYSTACK_SECRET_KEY, PAYSTACK_CURRENCY (default GHS), PAYSTACK_PLAN_{PRO,BUSINESS,ENTERPRISE}[_ANNUAL]. Billing returns 503 when the secret key is unset.

  • Added

    DMARC analyzer

    ingests DMARC aggregate XML reports (RFC 7489 RUA) submitted by receivers like Google, Microsoft, and Yahoo, and surfaces a pass/fail digest per source IP per domain on a new dashboard page. New module src/modules/dmarc/ exposes POST /v1/dmarc/reports (multipart upload, accepts raw .xml and gzipped .xml.gz up to 10 MB), GET /v1/dmarc/reports (cursor list), and GET /v1/dmarc/summary?domain=X&days=N (top 200 source IPs by volume, default 7 days, max 90). Parsing lives in src/lib/dmarc-parser.ts (fast-xml-parser + node:zlib for gz, with fallthrough for raw XML). A new dmarc-ingest BullMQ worker (src/workers/dmarc-ingest.ts) lets the inbound-email pipeline drop XML payloads onto a queue when a DMARC mailbox is configured. New tables dmarc_reports and dmarc_records (migration 0053_dmarc.sql plus an idempotent patch in scripts/apply-manual-migrations.ts) with inserts deduped on (org_id, reporter, report_id) so re-uploads are no-ops. Dashboard page at /deliverability/dmarc renders four summary cards (messages / authenticated / failed / pass rate), a per-source-IP table with DKIM + SPF pass counts and disposition badges, a recent-reports list, and an Upload report button. Docs at /docs/deliverability/dmarc. Closes GAPS #29.

  • Added

    Postmaster Tools — in-product "Connect with Google" OAuth flow

    new /v1/postmaster/oauth/{start,callback,status,disconnect} endpoints (mounted on postmasterModule) plus a Connect Gmail Postmaster dashboard page at /deliverability/postmaster-connect. GET /start returns a Google consent URL (scope https://www.googleapis.com/auth/postmaster.readonly, access_type=offline, prompt=consent) with a 10-minute Redis-backed state token bound to the active org. GET /callback exchanges the code via google-auth-library, persists the refresh / access token + verified email to four new postmaster_credentials columns (oauth_refresh_token, oauth_access_token, oauth_token_expires_at, oauth_user_email), and 302's back to the dashboard with ?connected=1. The postmaster-sync worker now resolves credentials in either mode: the legacy manual-paste columns (BYO GCP client) or the new in-product columns (refreshed against GOOGLE_OAUTH_CLIENT_ID/SECRET and cached until oauth_token_expires_at). Migration 0051 relaxes the original google_oauth_ NOT NULL constraints so rows created via the OAuth flow don't need per-org client credentials. New env vars: GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, GOOGLE_OAUTH_REDIRECT_URI (all optional — the dashboard CTA shows an "OAuth not configured" alert when unset and falls back to the manual paste flow). TODO(security): the new oauth_ token columns are still plaintext at rest, like the existing google_oauth_* columns; both should migrate to the DKIM envelope-encryption pattern.

  • Added

    AI in editor — subject / preview / alt-text generation + @mention references

    new /v1/ai/* module exposing POST /v1/ai/generate-subject, /v1/ai/generate-preview, /v1/ai/generate-alt-text, and GET /v1/ai/mention-search (Claude Haiku 4.5, multi-language). The dashboard editor surfaces a sparkles popover next to Subject and Preview inputs across Templates → New Template, Compose, and the Campaign Detail edit dialog. The popover ships with tone + language selectors, free-form prompt, and an @mention combobox that searches broadcasts / templates / automations in the caller's org. Mentions are server-resolved into the underlying content (subject + body snippet) before being sent to the model. Endpoints require full_access scope (mention search allows read_only) and return 503 ai_not_configured when ANTHROPIC_API_KEY is unset. Docs at /docs/dashboard/ai.

  • Added

    Domain verification — structured actionable guidance on failure

    POST /v1/domains/:id/verify now returns a checks array on every call. Each entry is self-contained and carries kind (spf / dkim / dmarc / mx / return_path), verified, record (fully-qualified DNS name being checked), actual (raw value found in DNS, or null), expected (the exact value the record must contain), issue (plain-language description of the problem), and next_step (plain-language fix instructions, including a propagation-wait hint). The shape replaces the previous keyed-object form (checks.spf / checks.dkim / checks.dmarc) with an array so future check kinds (MX, custom return path) can be appended without breaking the schema. The dashboard domain detail page (and the inline panel on the domains list page) renders the array as a checklist with the next-step text highlighted in an amber callout, the current DNS value, and a copy button for the expected value. Files: src/lib/dns/dns.ts, src/modules/domains/model.ts, dashboard/app/lib/types.ts, dashboard/app/routes/dashboard/deliverability/{domain-detail,domains}.tsx, docs-site/content/docs/api-reference/domains.mdx.

  • Added

    Automations dashboard step-builder UI (drag-and-drop)

    the automation detail page now renders steps in a sortable list backed by @dnd-kit/core + @dnd-kit/sortable. Drag the grip handle to reorder; changes persist via PATCH /v1/automations/:id/steps/:stepId (position only). A new click-to-add palette pre-selects the step type in the existing Add Step sheet. Reordering is disabled for active and archived automations so live runs don't observe mid-flight position changes. New components live in dashboard/app/components/automations/ (step-card.tsx, step-palette.tsx, sortable-step-list.tsx).

  • Added

    Automation run lifecycle webhooks

    automation.run.started, automation.run.completed, and automation.run.failed events now fire from the automation runner. started is dispatched once when a run transitions from pending to in_progress; completed / failed dispatch from finishRun() with timing + failure reason fields. Webhook dispatch failures log and continue so they never block the runner. Subscribe via the existing POST /v1/webhooks events array.

  • Added

    Cross-org domain & API key transfer

    new POST /v1/domains/:id/transfer and POST /v1/api-keys/:id/transfer endpoints move a resource between organizations the signed-in user owns or admins. Domain transfer preserves DNS state and blocks on name collision in the target org; API key transfer rotates the secret server-side so the previous holder loses access and returns the fresh key value exactly once. Dashboard exposes a Transfer dialog on the domain detail page and per-row Transfer button on the API keys page.

  • Added

    BIMI configuration UI on the domain detail page

    pairs with the existing POST /v1/domains/:id/bimi + GET + POST /:id/bimi/verify + DELETE endpoints. Owners can now enter a logo URL (HTTPS + .svg), an optional VMC URL (HTTPS + .pem), and a selector; the UI surfaces the resulting BIMI DNS record (host + value, copyable), VMC verification status, last-checked timestamp, plus Verify and Remove actions. Closes the gap that left BIMI configurable only via the API.

  • Added

    Email attachments end-to-end

    the dashboard compose flow now accepts file attachments (base64-encoded client-side, 10 MB total enforced both client- and server-side, blocked MIME types match the API). The email detail page lists attachment metadata (filename + MIME type). Server-side attachment validation gets dedicated unit tests covering blocked types, size limits, and MIME parameter handling.

  • Added

    Email attachment download API

    GET /v1/emails/:id/attachments returns attachment metadata; GET /v1/emails/:id/attachments/:index streams the raw bytes with Content-Type + Content-Disposition headers. Read-only scope, org-scoped.

  • Added

    Per-domain open/click tracking toggle

    domains.track_opens and domains.track_clicks (default true) settable on POST /v1/domains / PATCH /v1/domains/:id via a tracking: { opens, clicks } object. The email-sender worker reads them and skips the pixel/redirect when a domain has tracking disabled. Dashboard domain detail page exposes two Switch controls with optimistic toggling.

  • Added

    Schedule trigger picker in New Automation page

    adds Schedule as a selectable trigger type with a cron expression input and optional audience/segment targeting; previously the worker fired schedule triggers but the dashboard didn't expose them.

  • Added

    Attachment download buttons on the email detail page

    per-attachment Download icon next to filename + MIME badge; clicks /v1/emails/:id/attachments/:index directly.

  • Added

    Analytics scheduled report email handler

    notification-sender now handles "analytics-report" jobs queued by analytics-scheduler. Renders a new AnalyticsReport React Email template using the existing getDigestData aggregator and dispatches one notification-send job per recipient with daily dedup keys.

  • Added

    Cross-org Dead Letter Queue (DLQ) admin page

    new GET /v1/admin/dlq, GET /v1/admin/dlq/:id, POST /v1/admin/dlq/:id/retry, and DELETE /v1/admin/dlq/:id endpoints behind the superadmin guard. Dashboard route at /superadmin/dlq lists failed jobs with queue filter, payload + stack trace drawer, and one-click Retry / Delete. Retry re-enqueues into the original queue then deletes the row; if BullMQ rejects, the row stays.

  • Added

    Automations — schedule & manual triggers + plan limits

    adds the fireForSchedule and fireManual triggers, a new automation-scheduler worker that ticks every minute and fires schedule-triggered automations based on each automation's cron expression and last_fired_at, a self-contained UTC cron parser in src/lib/cron.ts, and a new automationRuns per-plan monthly quota wired through checkAutomationRunQuota (fail-open if the quota query itself errors). New POST /v1/automations/:id/trigger for manual runs.

  • Added

    CSV exports for contacts / emails / campaigns / segments

    new POST /v1/exports, GET /v1/exports, GET /v1/exports/:id, and GET /v1/exports/:id/download endpoints. Background csv-exporter worker streams rows in batches of 1000 and writes CSV files to tmp/exports/. Exports expire after 7 days. Dashboard list pages get an Export button that creates, polls, and downloads in one click.

  • Added

    Gmail Postmaster Tools integration

    new postmaster_credentials and postmaster_metrics tables, daily postmaster-sync worker that refreshes OAuth tokens and pulls per-domain traffic stats (spam rate, IP/domain reputation, DKIM/SPF/DMARC pass rates, encryption ratios), and a new /v1/postmaster/{credentials,metrics,sync} module. Deliverability → Reputation page renders a Postmaster section when metrics exist, otherwise shows a connect-CTA.

  • Added

    Visual editor — code, chart, and tweet blocks

    adds three new block types: a code block with a language selector, a chart block that renders a self-contained inline SVG bar/line/area chart, and a tweet block that renders a static X post card (avatar, author, body, "View on X" link). Slash menu picks them up automatically via slashTriggers.

  • Added

    Visual editor — oEmbed cache, CodeMirror HTML view, AI seams in blocks

    closes the Phase 3-5 backlog for the visual editor. (1) The tweet block now resolves URLs through POST /v1/oembed/resolve, which is backed by a new per-org oembed_cache table (migration 0052_oembed_cache.sql) with a 7-day TTL and supports YouTube, Twitter/X, and Vimeo. Cache misses hit publish.twitter.com with a static-preview safety net when X returns 401/403. (2) The existing HTML code-view panel is upgraded to CodeMirror (@uiw/react-codemirror + @codemirror/lang-html) with syntax highlighting, fold gutter, and Prettier (standalone) formatting on click. (3) Blocks now accept an optional onAiRequest?: (kind: 'subject' \| 'preview' \| 'alt') => void prop wired through BlockPropertyFormProps → PropertyPanel → VisualEditorPage. The image block surfaces a sparkle AI button next to the Alt-text field, the tweet block surfaces one next to Body; a separate AI-editor agent will swap the editor's placeholder handler for the real Anthropic-backed service.

  • Added

    Automated database backups → Cloudflare R2

    nightly pg_dump -Fc of the prod Postgres container streamed directly to an R2 bucket via rclone (no local file — the prod disk runs hot). Two tiers: db/daily/ kept 14 days, db/monthly/ (promoted on the 1st) kept 365 days. New scripts/backup-db.sh (cron, 03:30 UTC), scripts/restore-db.sh (--list / --latest / explicit object, typed-name confirmation, --force for automation), and idempotent scripts/setup-backup.sh (installs rclone, writes chmod-600 rclone.conf, installs the cron entry, runs one verification backup). Credentials live in server-only /opt/sendry/.backup.env (gitignored; .backup.env.example documents the contract). Closes the "zero backup infrastructure / unrecoverable on data loss" gap. Docs at docs/BACKUP.md.

  • Improved

    Billing runs through Paystack; Stripe disabled for now

    the dashboard upgrade flow now POSTs to /v1/billing/paystack/checkout (was /v1/billing/checkout) and redirects to Paystack's hosted checkout. Since Paystack has no self-serve billing portal, "Manage Subscription" became "Cancel Subscription" (confirm dialog → POST /v1/billing/paystack/cancel; downgrade lands on the subscription.disable webhook), and the Payment Method / Billing History cards are now informational (card collected at checkout, receipts emailed) instead of linking to a Stripe portal. API client: createBillingPortal() → cancelSubscription() (dashboard/app/lib/api/billing.ts). Stripe copy on the pricing FAQ replaced with Paystack (cards, mobile money, bank transfer). The Stripe module + endpoints remain in the codebase but are dormant — STRIPE_SECRET_KEY is commented out in production, so /v1/billing/checkout and /portal return 503. Re-enable by restoring the key and pointing the client back.

  • Improved

    Authentication panel — BIMI status is tri-state

    GET /v1/deliverability/reports/... now returns authentication.bimi: "verified" | "failed" | "not_configured" instead of a boolean. The dashboard renders an unobtrusive gray "Not configured" indicator when an org has no BIMI record set up, reserving the red X for actual DNS failures. Removes the misleading "always red" BIMI badge for orgs that haven't opted into BIMI.

  • Improved

    Campaign detail surfaces names, not raw IDs

    Audience ID and Template ID rows on the campaign detail page now display the resource name as a clickable link to the audience / template detail page, with the raw ID kept only as a fallback when the name can't be resolved. Automation run detail email IDs are also clickable links to the email detail page.

  • Fixed

    Lower-severity hardening pass (SEC-L1/L2/L3/L5/L6)

    (1) Mailgun webhook signature verification now uses crypto.timingSafeEqual and rejects timestamps more than 5 minutes from now, limiting replay of captured webhooks. (2) POST /v1/emails caps to/cc/bcc arrays at 50 entries so an oversized recipient list can't be stored before the provider rejects it. (3) CSV exports prefix any field beginning with =, +, -, @, tab, or CR with a single quote to neutralize spreadsheet formula injection (numbers untouched; covers contact/email/analytics exports via the shared csvEscape, now unit-tested). (4) CORS no longer trusts http://localhost:3001 as a credentialed origin in production. (5) The pino base logger redacts password/secret/token/apiKey/authorization/cookie paths as defense-in-depth.

  • Fixed

    Unsubscribe links are now HMAC-signed

    list unsubscribe URLs were base64url(orgId|email|listId) with no signature, and the orgId is visible in every marketing email, so an attacker could craft tokens for arbitrary addresses and mass-unsubscribe a competitor's list. buildUnsubscribeUrl now signs tokens with signToken; the public /unsubscribe/:token GET/POST verify the signature via the new decodeUnsubscribeToken. Legacy unsigned links already in sent mail still work (a compliance requirement) and can be turned off with UNSUBSCRIBE_REJECT_UNSIGNED=1 once old mail has aged out. RFC 8058 one-click is preserved (SEC-M2).

  • Fixed

    Org invites no longer auto-activate before the email is proven

    databaseHooks.user.create.after activated every pending membership matching the new user's email immediately, so registering with a victim's invited address joined their org without proving control of it. Invite activation now runs only once the address is verified: at creation for provider-verified social logins, and via emailVerification.afterEmailVerification for email/password signups. Login itself is unchanged (verification is not forced), so no existing users are locked out (SEC-M4).

  • Fixed

    SSRF hardening on webhook + inbound delivery

    validateWebhookUrl only string-matched the hostname, so a public DNS name pointing at 169.254.169.254/RFC1918 (DNS rebinding), an alternate-encoding IP literal, or an IPv4-mapped IPv6 ([::ffff:169.254.169.254]) bypassed it, and fetch followed redirects to unvalidated targets. Added assertPublicWebhookUrl() (used at delivery time in webhook-delivery and inbound forwarding) which resolves DNS and rejects if any resolved address falls in a private/reserved range (loopback, RFC1918, link-local, CGNAT, ULA/link-local IPv6, multicast, reserved, IPv4-mapped). Delivery fetches now use redirect: "manual". Registration keeps the synchronous structural check for fast feedback (SEC-M1).

  • Fixed

    brand_color injection on the public unsubscribe page

    the org branding color is interpolated unescaped into a <style> block on the Sendry-hosted unsubscribe page; an org could store red;}</style>… for CSS/markup injection (JS already blocked by CSP). Now validated to a strict 6-digit hex on write, and pageShell coerces any non-hex (legacy) value to a safe default on render (SEC-M3).

  • Fixed

    Security headers on dashboard + marketing responses

    entry.server.tsx now sets X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy, Content-Security-Policy: frame-ancestors 'none', and HSTS in production. Closes the clickjacking gap on the authenticated dashboard (the API already set these). A full resource CSP (nonce-based for React hydration) is tracked separately (SEC-M8).

  • Fixed

    React Router bumped 7.12.0 → 7.17.0 to clear an unauth RCE + DoS/XSS chain

    the dashboard's framework had several high-severity advisories: arbitrary constructor invocation (unauth RCE) via the vendored turbo-stream TYPE_ERROR deserialization (GHSA-49rj-9fvp-4h2h), DoS via unbounded path expansion in the __manifest endpoint (GHSA-8x6r-g9mw-2r78), DoS via reflected input in single-fetch (GHSA-rxv8-25v2-qmq8), and two XSS paths in RSC/prerender redirect handling (GHSA-8646, GHSA-f22v). Surgical minor bump within the v7 line; the build + typecheck pass and bun audit no longer reports any react-router-core advisory. (Remaining react-router-tagged audit entries are dev/build-tooling transitives — vite, lodash, picomatch under @react-router/dev — and express transitives, none on the prod request path.)

  • Fixed

    DMARC XML parser hardened against entity-expansion DoS

    src/lib/dmarc-parser.ts parses attacker-supplied aggregate reports (uploaded XML / inbound mail). Set processEntities: false (DMARC reports never use XML entities) so "billion laughs"–style entity expansion is impossible regardless of fast-xml-parser version. The installed direct dependency is already 5.7.2 (past the patched 5.7.0); the <5.7.0 advisory the scanner reports is aws-sdk's transitive XML builder, which only serializes our own outbound AWS requests, not untrusted input.

  • Fixed

    Privilege escalation: org members could seize full team control via API keys

    the session/API-key resolver hard-coded memberRole: "owner" on every API key, while team-management endpoints (POST /v1/team/invite, DELETE /v1/team/:id, PATCH /v1/team/:id/role) gated only on memberRole. A low-privilege member (who already carries full_access scope, enough to create an API key) could mint a key and use it to invite admins, remove owners, and change roles. Fixed: API keys now resolve to a non-admin memberRole, and team mutations route through requireTeamManager, which additionally rejects API-key callers outright — team management is now a signed-in-user action only. Found in the 2026-06-10 security audit (docs/audits/2026-06-10-security-audit.md, SEC-H1).

  • Fixed

    Dashboard session secret silently fell back to a public default

    sessions.server.ts used process.env.BETTER_AUTH_SECRET ?? "dev-secret", so a missing secret in production would sign session cookies with a globally-known string, making them forgeable. The dashboard now throws on boot in production when BETTER_AUTH_SECRET is unset and never falls back to a default (SEC-H2).

  • Fixed

    All device session tokens were serialized into the account page

    the account loader returned the raw bearer token for every active session into the SSR/hydration payload, so any XSS, extension, or page-source read yielded valid hijack tokens for all of a user's devices. The loader now exposes only session ids; the revoke action resolves id → token server-side (SEC-M6).

  • Fixed

    Open redirect on the org selector

    select-organization's next param flowed straight into window.location.href; it is now validated to be a same-origin relative path (rejects //, \, and any scheme) (SEC-M5).

  • Fixed

    **/v1/* dashboard proxy hardened** — the proxy forwarded all client headers (allowing spoofed X-Forwarded-For/X-Real-IP) and could be escaped past /v1/ via ../encoded-slash path tricks.

    It now forwards an explicit header allowlist and rejects traversal, asserting the resolved target stays under /v1/ (SEC-M7).

  • Fixed

    Marketing contact form was fake

    the /contact form did nothing but flip to a "Message queued" success state on submit (no name/required attrs, no delivery). It is now a real React Router action backed by a new public POST /v1/contact endpoint that validates input, applies a per-IP throttle (5 per 10 min) on top of the global limiter, drops bot submissions via a honeypot field, and delivers through the system mailer (the already-verified noreply@sendry.online identity — no customer API key or verified customer domain required) to the inbox set by the new CONTACT_TO env var, with the submitter as reply-to. The UI now shows genuine pending / success / error states (UX-H1).

  • Fixed

    Campaign "Send Now" could double-fire and hid the recipient count

    the campaign detail page's send confirmation was a plain Dialog with no in-flight guard, so a double-click or slow-network retry could dispatch the broadcast twice; neither the detail nor the list confirmation told you how many recipients would receive it. Both are now AlertDialogs that state "send to N recipients", disable their buttons while the request is in flight, and guard against re-entry (UX-H6 / UX-H7).

  • Fixed

    Verified domains rejected at SES with "Email address is not verified"

    when a SES email identity already existed at CreateEmailIdentity time (race with another org, repeat verify() call, or — most commonly — an identity created earlier with AWS_SES-managed DKIM tokens that were never published to DNS), we swallowed AlreadyExistsException and silently accepted the pre-existing identity as verified. SES kept polling unpublished AWS_SES token CNAMEs forever, leaving VerificationStatus: FAILED / DkimAttributes.Status: FAILED / VerifiedForSendingStatus: false, so every send was rejected with the "identity not verified" error even though our dashboard showed all three DNS checks green. Fixed: on AlreadyExistsException we now call PutEmailIdentityDkimSigningAttributes with SigningAttributesOrigin: EXTERNAL to force the identity onto our BYO DKIM selector + private key (matches the record actually published at <selector>._domainkey.<domain>). Applied across all three SES code paths (regional client, regional-domain-with-default-region mirror, default-client fallback). Also strips PEM headers/footers from the key before sending — SES expects raw base64.

  • Fixed

    Mailpit dev provider registered in production

    MailpitProvider.isConfigured() returned true when MAILPIT_ENABLED=true regardless of NODE_ENV, and prod .env.docker carried MAILPIT_ENABLED=true left over from copy-paste of the compose env. After SES failed (e.g. unverified identity), the provider chain fell through to mailpit, which tried to SMTP to host mailpit (only resolvable inside the docker-compose dev network) and threw getaddrinfo ESERVFAIL. Real mail then bounced to DLQ via a dev capture trap instead of surfacing the SES error. Hard-guarded: mailpit refuses to register when NODE_ENV=production regardless of any env var. Stripped MAILPIT_* from prod .env.docker as defense in depth.

  • Fixed

    API crash loop on every SMTP client disconnect

    src/smtp/server.ts declared onClose(session, callback) and invoked callback(), but the underlying smtp-server library calls onClose(session) with no callback (see node_modules/smtp-server/lib/smtp-connection.js:485). Every disconnect threw TypeError: callback is not a function and crashed the api container; the auto-restart loop made unrelated HTTP requests (e.g. POST /v1/domains reported as "Internal Service Error" by a user) surface as 500s during the brief windows the api was down. Fixed by dropping the bogus callback param; SMTP cleanup is purely synchronous now.

  • Fixed

    Delete-confirmation pass — every destructive dashboard action now opens an AlertDialog

    the Domains list page deleted a domain immediately on click with no prompt (reported by a user); fixed by routing it through a shadcn AlertDialog that names the domain, warns the action is irreversible, and uses a destructive Confirm button. Same treatment applied to every other delete / remove action in the dashboard that was missing or weakly guarded: Templates (immediate delete on icon click), Suppression list entry remove, Unsubscribe list entry remove, Audience contact remove (/audiences/:id), Segment contact remove (/segments/:id), Dedicated IP domain-assignment remove (/dedicated-ips), Gmail Postmaster disconnect (/deliverability/postmaster-connect, was using window.confirm), and Account session revoke (/account, was a one-click POST form). Pages that previously used a plain Dialog for the confirm step (Webhook delete, Contact delete, Audience delete, Segment delete, Campaign delete, BIMI remove, Custom contact property delete) were migrated to AlertDialog for consistency and given destructive-styled action buttons, in-flight disabled state, and toast.success / toast.error feedback that names the specific resource. The Topics delete dialog gained the destructive variant and an in-flight disabled state. Files: dashboard/app/routes/dashboard/deliverability/{domains,domain-detail,dedicated-ips,webhooks,postmaster-connect}.tsx, dashboard/app/routes/dashboard/sending/{templates,campaign-detail}.tsx, dashboard/app/routes/dashboard/contacts/{suppression,unsubscribes,audience-detail,segment-detail,contact-detail,topics}.tsx, dashboard/app/routes/dashboard/settings/{settings.contact-properties,account}.tsx.

  • Fixed

    Broken Sendry logo in system emails

    _layout.tsx was pointing at https://sendry.online/logo.png, which redirects to /login because no such file is shipped from the dashboard. Added a dashboard/public/email-logo.png (600×180 transparent PNG generated from the brand SVG) and pointed the email layout at /email-logo.png so every transactional / system email (welcome, verify-email, team-invite, alerts, summaries, etc.) renders the logo correctly in Gmail, Outlook, and Apple Mail.

  • Fixed

    Multi-org users blocked on the select-organization screen

    the global apiKeyAuth resolver was throwing 409 active_org_required against GET /v1/organizations and POST /v1/organizations/switch, so the picker received an error instead of a list and rendered "No organizations found for your account." Those two paths are now exempt from the global resolver, and the select-organization loader also falls back to error.details.available_orgs if a 409 still slips through. Multi-org sign-in now works end-to-end again.

v2.9.0May 11, 2026

Release highlights

1 update in this release, covering 1 addition.

Added1
Improved0
Fixed0
  • Added

    Campaign recipients preview before send

    GET /v1/campaigns/:id/recipients now falls back to a preview drawn from the campaign's audience or segment when no campaign_recipients rows exist yet, so draft / scheduled campaigns show who the campaign will be sent to instead of an empty table. Preview rows carry preview: true and a pending status; the dashboard recipients tab surfaces them with a "Pending send" badge and an info banner.

v2.8.0May 10, 2026

Release highlights

1 update in this release, covering 1 improvement.

Added0
Improved1
Fixed0
  • Improved

    API key prefix rebranded re_ → sn_live_ / sn_test_

    newly generated API keys now use a Sendry-branded prefix and embed the key mode (live or test) directly in the key string, matching the Stripe-style convention. Existing re_* keys continue to work — authentication uses a SHA-256 hash lookup, not the prefix. The displayed key_prefix in the dashboard now shows sn_live_AbCd / sn_test_AbCd (12 chars) instead of re_AbCde (8 chars).

v2.7.2May 10, 2026

Release highlights

1 update in this release, covering 1 fix.

Added0
Improved0
Fixed1
  • Fixed

    Domain create error message

    when the production server is missing DKIM_KEK_ARN (the AWS KMS key used to wrap each domain's DKIM private key), POST /v1/domains now returns 503 dkim_kek_not_configured with a clear "DKIM key encryption is not configured" message instead of bubbling up a raw 500 that the dashboard rendered as the generic "An unexpected error occurred."

v2.7.1May 10, 2026

Release highlights

2 updates in this release, covering 1 improvement, 1 fix.

Added0
Improved1
Fixed1
  • Improved

    Recipients table on email detail

    replaced the per-recipient Tabs/Select selector with a recipients table showing every recipient with their per-recipient status, delivered/opened/clicked/bounced/complained counts, and last activity. Click any row to expand the timeline for that recipient inline. The combined "All events" timeline still renders below the table. Tabs did not scale beyond a handful of recipients.

  • Fixed

    SES delivery webhook only recorded the first recipient

    handleDelivery previously took delivery.recipients[0], dropping every recipient after the first. Multi-recipient emails (To/Cc/Bcc) now record a delivered event for each recipient, matching the existing per-recipient behavior of bounce and complaint handlers.

v2.7.0May 10, 2026

Release highlights

5 updates in this release, covering 4 additions, 1 fix.

Added4
Improved0
Fixed1
  • Added

    Per-recipient events on email detail

    emails sent to multiple recipients now show a recipient selector above the Events timeline (Tabs for ≤4 recipients, Select dropdown for more). Filter the timeline by recipient and see per-recipient delivered/opened/clicked/bounced/complained counts. Recipients with zero events still appear so you can see who is yet to act.

  • Added

    Template picker on Compose

    the single-send Compose page now has a "Template (optional)" selector that lists saved templates with their engine (html / react / visual). Selecting a template loads the subject and HTML, exposes inputs for any declared variables, and sends with template_id + variables so the server does the canonical render.

  • Added

    Template field on Broadcast Campaign edit

    the Edit Draft form now has a "Template (optional)" selector; selected templates are persisted via template_id and rendered by the campaign-sender worker at send time. Inline HTML/text fields are dimmed when a template is selected. The overview tab now resolves and shows the template's name instead of just the raw ID.

  • Added

    Template picker in Automation builder

    the send_email step config in the automation builder replaced the raw "Template ID" text input with a real Template selector showing template name + engine badge. Falls back to the text input if templates fail to load so adding a step is never blocked.

  • Fixed

    Side drawer padding

    SheetContent (used by the automation step drawer, superadmin user drawer, and others) now has sane internal padding by default. Form fields no longer sit flush against the drawer edges. Drawers that need full-bleed layout (mobile sidebar nav, inbound email drawer, test-inbox drawer) are unaffected — they explicitly opt out with p-0.

v2.6.0May 10, 2026

Release highlights

8 updates in this release, covering 6 additions, 2 fixes.

Added6
Improved0
Fixed2
  • Added

    Automations

    event-triggered multi-step email workflows. Build flows from send_email, wait, branch (on contact property), and ab_split steps; trigger them from custom events or when a contact is added to a segment. Re-entry policies (once_per_contact, cooldown, always) control how repeat triggers are handled. Build and manage flows visually from the dashboard at /automations, with per-automation stats covering total, active, completed, and failed runs. Available on Pro and above.

  • Added

    POST /v1/events

    fire arbitrary events from your app to trigger automations; events are matched against active automation triggers and enrolled contacts begin their flow.

  • Added

    GET /v1/automations/:id/runs

    per-contact execution history with status, current step, and completed-at timestamps; supports cursor-based pagination.

  • Added

    Automations marketing page

    dedicated /features/automations deep-dive plus entries in the features grid, pricing comparison, and Product nav dropdown.

  • Added

    Visual Editor and Broadcast Campaigns added to the Product nav dropdown so all dedicated feature pages are reachable from the top-level navigation.

  • Added

    Docs site

    new /automations and /visual-editor guides covering concepts, step types, and API references.

  • Fixed

    Automation stats counters stuck at 0

    total_runs, active_runs, completed_runs, and failed_runs on the automations table now increment correctly on run create and finish; previously they stayed at zero even after successful runs.

  • Fixed

    "View Runs" button missing on archived/draft automations

    the View Runs link is now visible on every automation regardless of status.

v2.5.0May 7, 2026

Release highlights

18 updates in this release, covering 7 additions, 11 fixes.

Added7
Improved0
Fixed11
  • Added

    Inbound email address per org

    every account now has a dedicated {slug}@recv.sendry.online routing address. Custom domains continue to work as before. Inbound email receiving requires a Pro plan or higher.

  • Added

    Inbound dashboard redesign

    replaced the tab-based layout with a streamlined list view. Click any row to open a side drawer showing full email details: HTML preview (sandboxed), plain text, email headers, and attachment metadata. Webhook configuration is now in a dedicated "Configure" dialog.

  • Added

    Status filter on emails list

    filter sent emails by delivery status (delivered, bounced, complaint, failed, pending) directly in the dashboard.

  • Added

    Email search on unsubscribes

    filter the unsubscribes list by email address with debounced live search.

  • Added

    Template name search

    filter the templates list by name in the dashboard.

  • Added

    GET /v1/campaigns/:id/recipients

    list all recipients for a campaign with their individual delivery status; supports cursor-based pagination and a status filter.

  • Added

    GET /v1/emails/:id/events

    list delivery events (opens, clicks, bounces, complaints) for any email; supports cursor-based pagination and an optional type filter.

  • Fixed

    Template variables not applied

    emails sent with template_id now correctly substitute {{variable}} tokens in both HTML and plain-text bodies; previously variables were silently dropped.

  • Fixed

    subject incorrectly required with a template

    subject is now optional when template_id is provided, since the template already defines its own subject line.

  • Fixed

    open_rate exceeding 100%

    analytics rates (open_rate, click_rate, bounce_rate, etc.) are now clamped to [0, 1]; previously they could exceed 1.0 when event counts outpaced delivered emails.

  • Fixed

    scheduled_at lost on draft campaign creation

    the scheduled send time is now stored when creating a campaign with send: false; previously it was silently discarded.

  • Fixed

    Inbound email headers corrupted

    headers were being serialized as [object Object] due to mailparser returning structured objects instead of raw strings. Fixed at both the storage layer (using headerLines) and the display layer (legacy records filtered in the UI).

  • Fixed

    Inbound SNS integration

    resolved multiple issues: subscription confirmation now works regardless of Content-Type, webhook registration no longer conflicts with the global auth middleware, and SNS notification bodies are correctly parsed as JSON.

  • Fixed

    Email list ordering

    emails now sort strictly by creation time; previously the nanoid-based ID ordering could produce incorrect sequences.

  • Fixed

    Bulk contact import

    audience ID is now validated before import begins; validation error messages across the API are cleaner and more actionable.

  • Fixed

    Campaign plan-limit handling

    plan-blocked campaigns now surface a clear error; SES tag limits are correctly enforced when sending campaign emails.

  • Fixed

    Billing upgrade prompt

    the "Upgrade to Pro" button is now hidden for Business and Enterprise plan users; the Pro plan label is displayed correctly in the nav.

  • Fixed

    SMTP relay display

    the relay dashboard now shows the correct server hostname and confirms port 2525 for authenticated SMTP connections.

v2.4.0May 7, 2026

Release highlights

13 updates in this release, covering 3 additions, 2 improvements, 8 fixes.

Added3
Improved2
Fixed8
  • Added

    Compose email from dashboard

    write and send emails directly from the Sendry dashboard with an HTML editor, plain-text fallback, live preview pane, and full To / CC / BCC / Reply-To support

  • Added

    Password reset

    users can request a reset link by email and set a new password from a secure token-gated page

  • Added

    Social login

    sign in with Google or GitHub in addition to email and password

  • Fixed

    Open tracking blocked

    the tracking pixel endpoint was incorrectly caught by the API authentication middleware; opens and clicks now record correctly for all emails

  • Fixed

    Analytics showing zero opens

    direct consequence of the tracking fix; the analytics overview now reflects real open events

  • Fixed

    DKIM verification failing

    corrected DNS lookup logic that failed when registrars (e.g. Namecheap) split DKIM public keys across multiple TXT records

  • Fixed

    Delivery events not recording

    the SES webhook handler now supports both SES v1 (notificationType) and SES v2 (eventType) formats, and strips the @region.amazonses.com suffix from message IDs so delivery, bounce, and complaint events match correctly

  • Fixed

    Notification links pointing to API

    welcome emails, domain-verified alerts, and billing emails now link to the dashboard instead of the raw API server

  • Fixed

    Breadcrumbs showing "Dashboard" everywhere

    all sidebar pages now display their correct title in the breadcrumb navigation

  • Fixed

    Unknown URLs showing a bare 404

    navigating to an invalid path now renders a friendly "Page not found" screen inside the dashboard layout

  • Fixed

    SMTP relay hostname blank

    the SMTP configuration panel now shows the correct server hostname

  • Improved

    Smarter open tracking

    known email prefetch agents (Gmail Image Proxy, Apple Privacy Proxy, Outlook, Microsoft 365, Barracuda, Proofpoint, Mimecast, and generic crawlers) are filtered out so only genuine user opens are counted

  • Improved

    Open event deduplication

    each email records at most one open event regardless of how many times the pixel is fetched

v1.6.1May 6, 2026

Release highlights

1 update in this release, covering 1 fix.

Added0
Improved0
Fixed1
  • Fixed

    Analytics date range

    same-day events were silently excluded from all analytics queries because date-only strings were parsed as midnight UTC; queries now use full ISO timestamps

v2.3.0April 5, 2026

Release highlights

10 updates in this release, covering 10 additions.

Added10
Improved0
Fixed0
  • Added

    Natural-language scheduling

    scheduled_at now accepts plain English like "in 2 hours", "Friday at 3pm ET", or "tomorrow morning" in addition to ISO 8601 across all send paths (single, batch, patch, and campaigns)

  • Added

    30-day scheduling cap

    maximum advance scheduling is capped at 30 days consistently across all send endpoints

  • Added

    Multiple workspaces

    create additional organizations from the org switcher; manage all workspaces from Settings → Workspaces

  • Added

    Deliverability insights

    GET /v1/emails/:id/insights returns a quality score (0–100) across 7 checks; the email detail page now has an Insights tab with a score ring and actionable per-check results

  • Added

    Per-recipient webhook fan-out

    email.sent webhooks now fire once per recipient with a single-address to field for accurate attribution on multi-recipient sends

  • Added

    Plain-language bounce reasons

    bounces include a human-readable reason, description, action, and retryable flag instead of raw DSN codes

  • Added

    TLS policy per domain

    set tls_policy to "opportunistic" or "enforced" per domain via PATCH /v1/domains/:id or the domain settings panel

  • Added

    Email sharing

    generate a 48-hour shareable preview link for any email from the list or detail page

  • Added

    Bulk actions

    select multiple contacts or API keys for bulk delete via a floating action bar

  • Added

    Contact activity timeline

    the contact detail page shows a paginated history of all email events for that contact

v2.2.0April 5, 2026

Release highlights

6 updates in this release, covering 6 additions.

Added6
Improved0
Fixed0
  • Added

    Custom contact properties

    define typed fields (string, number, boolean, date) per organization and set values per contact; manage definitions from Settings → Contact Properties

  • Added

    Property variables in campaigns

    use {{property_name}} tokens in campaign HTML or text; values are substituted per recipient at send time

  • Added

    Segments

    create contact segments, manage membership, and target campaigns at a segment instead of an audience

  • Added

    Topics & unsubscribes

    create email topics in opt-in or opt-out mode; filter sends to recipients subscribed to a topic; manage topics from the dashboard

  • Added

    Template versioning

    every template save creates a version record; restore any previous version from the version history panel in the template editor

  • Added

    Template duplication

    clone any template with one click from the templates list

v2.1.0April 5, 2026

Release highlights

9 updates in this release, covering 9 additions.

Added9
Improved0
Fixed0
  • Added

    Idempotency keys

    send an Idempotency-Key header on POST /v1/emails or batch sends; duplicate requests within 24 hours return the cached response without re-sending

  • Added

    Inline image attachments

    attachments support a content_id field for CID-based inline embedding; reference in HTML as <img src="cid:YOUR_ID">

  • Added

    Auto plain-text generation

    when html is provided without text, a plain-text version is generated automatically

  • Added

    Batch permissive mode

    POST /v1/emails/batch accepts batch_validation: "permissive" to process valid emails and return per-item errors instead of failing the entire batch

  • Added

    Schedule updates

    PATCH /v1/emails/:id lets you update the subject, body, or scheduled time of a pending email before it sends

  • Added

    Webhook delivery log

    GET /v1/webhooks/deliveries lists all delivery attempts; replay any failed delivery with POST /v1/webhooks/deliveries/:id/replay

  • Added

    Contact & domain webhook events

    webhooks now fire contact.created, contact.updated, contact.deleted, domain.created, domain.updated, and domain.deleted

  • Added

    Campaign create-and-send shortcut

    POST /v1/campaigns accepts send: true to create and dispatch or schedule a campaign in one request

  • Added

    API request logs

    every authenticated API call is logged; browse from the dashboard at /api-logs with method, path, status, and duration

v2.0.0April 4, 2026

Release highlights

3 updates in this release, covering 3 additions.

Added3
Improved0
Fixed0
  • Added

    SMTP relay dashboard

    view connection details and credentials for drop-in SMTP integration at /smtp

  • Added

    Inbound email dashboard

    configure inbound routing and browse received emails at /inbound

  • Added

    Scheduled analytics reports

    Business and Enterprise plans can schedule email digest reports of analytics data on daily, weekly, or monthly cadences

v1.8.0March 12, 2026

Release highlights

9 updates in this release, covering 5 additions, 4 fixes.

Added5
Improved0
Fixed4
  • Added

    TypeScript SDK

    27 fully typed resource classes with JSDoc examples covering all API surfaces

  • Added

    Python SDK

    synchronous and async clients with full type hints and webhook signature verification

  • Added

    Go SDK

    idiomatic Go client with struct options, automatic retry (3×), and webhook verification

  • Added

    Documentation site

    comprehensive docs at [docs.sendry.online](https://docs.sendry.online) covering quickstart, API reference, SDK guides, and how-tos

  • Added

    Dead-letter queue

    permanently failed email and webhook jobs are preserved for inspection rather than silently dropped

  • Fixed

    API key scope hierarchy was inverted; full_access now correctly supersedes sending_access which supersedes read_only

  • Fixed

    Free-plan orgs could bypass the email limit by sending overages without a payment method on file

  • Fixed

    Unauthenticated endpoints had no rate limiting; a per-IP limit now applies to all public routes

  • Fixed

    Payment failures now suspend billing correctly; sending is restored automatically when payment succeeds

v1.7.0March 5, 2026

Release highlights

4 updates in this release, covering 2 additions, 2 fixes.

Added2
Improved0
Fixed2
  • Added

    Log retention

    email logs and analytics rollups are automatically pruned based on your plan's retention window

  • Added

    Campaign plan limits

    Free plans are capped at 3 campaigns and 3 audiences; Pro at 25 each; Business and Enterprise are unlimited

  • Fixed

    Dashboard billing, account, and notifications pages failed when running in production Docker because they used a hardcoded localhost API URL

  • Fixed

    Webhook delivery had 0 retries by default, meaning a single network error permanently lost an event; now retries 3× with exponential backoff

v1.6.0March 1, 2026

Release highlights

4 updates in this release, covering 4 additions.

Added4
Improved0
Fixed0
  • Added

    Test-mode API keys

    create keys with mode: "test" to send emails without domain verification; captured emails appear in the Test Inbox at /test-inbox

  • Added

    Test Inbox

    browse and preview all test-mode emails from the dashboard

  • Added

    Multi-organization switching

    switch between organizations from the sidebar without logging out

  • Added

    Email verification

    new accounts receive a verification email on sign-up; an in-dashboard banner reminds unverified users

v1.5.0February 27, 2026

Release highlights

6 updates in this release, covering 6 additions.

Added6
Improved0
Fixed0
  • Added

    Overage billing

    Pro and Business plans auto-bill usage above the monthly limit in 1,000-email blocks instead of hard-stopping

  • Added

    Annual billing

    switch to annual on the pricing or billing page and save roughly two months per year

  • Added

    Usage endpoint

    GET /v1/billing/usage returns sent volume, limits, overages, and current billing period details

  • Added

    Approaching-limit alerts

    receive an email notification when usage crosses 80% of the monthly quota

  • Added

    SLA monitoring

    the status page now shows per-component SLA targets, current SLA compliance, and hourly latency history

  • Added

    Status alerts

    email notifications fire when a component goes down, recovers, breaches its SLA, or degrades in latency

v1.4.0February 25, 2026

Release highlights

7 updates in this release, covering 7 additions.

Added7
Improved0
Fixed0
  • Added

    Broadcast campaigns

    create, schedule, and send email campaigns to contact audiences with per-campaign analytics

  • Added

    Visual email editor

    drag-and-drop block-based template builder with live preview and HTML export

  • Added

    Dedicated IPs

    provision and manage dedicated IP addresses with SES pool management and warm-up tracking (Business+)

  • Added

    Deliverability suite

    blocklist monitoring across 50+ DNSBLs, IP reputation snapshots, and BIMI record management (Pro+)

  • Added

    Multi-region sending

    configure sending across multiple AWS SES regions with per-domain region assignment (Business: 3 regions, Enterprise: unlimited)

  • Added

    Public status page

    real-time component health, uptime history, and incident management at /status

  • Added

    Advanced analytics

    cohort analysis, peer benchmarks, analytics export (CSV/JSON), and scheduled reports (Business+)

v1.3.0February 25, 2026

Release highlights

5 updates in this release, covering 5 additions.

Added5
Improved0
Fixed0
  • Added

    Contacts management

    create, update, delete, and bulk import contacts with custom metadata; plan-based limits apply

  • Added

    Audiences

    organize contacts into lists for targeted sending

  • Added

    TypeScript SDK

    @sendry/sdk with full API coverage, retry logic, and typed responses

  • Added

    Notification emails

    welcome, domain verified, bounce/complaint alerts, and payment receipt emails

  • Added

    Dark mode

    toggle between light and dark themes on all pages

v1.2.1February 25, 2026

Release highlights

4 updates in this release, covering 2 additions, 2 fixes.

Added2
Improved0
Fixed2
  • Added

    Role-based team management

    only owners and admins can invite members, remove teammates, or change roles

  • Added

    Invite confirmation

    pending invites auto-activate when an invited user signs up

  • Fixed

    Organization owner now appears in the team members list

  • Fixed

    Branding settings no longer fail when optional fields are left blank

v1.2.0February 25, 2026

Release highlights

3 updates in this release, covering 3 additions.

Added3
Improved0
Fixed0
  • Added

    Team collaboration

    invite teammates, manage roles (owner, admin, member), and track seat usage (Pro: 5 seats, Business: 20, Enterprise: unlimited)

  • Added

    Custom unsubscribe pages

    personalize the unsubscribe experience with your brand logo, colors, message, and redirect URL from the Branding settings page

  • Added

    Unsubscribes dashboard

    browse and manage all marketing opt-outs from /unsubscribes

v1.1.0February 25, 2026

Release highlights

5 updates in this release, covering 3 additions, 2 improvements.

Added3
Improved2
Fixed0
  • Added

    Multi-provider routing

    automatic failover between email providers; transactional email routes through SES, marketing through Mailgun

  • Added

    Marketing email endpoint

    POST /v1/emails/marketing with required unsubscribe URL for CAN-SPAM and GDPR compliance

  • Added

    One-click unsubscribe

    RFC 8058 compliant unsubscribe pages

  • Improved

    Updated pricing: Pro $18/month, Business $79/month

  • Improved

    Overage rates: Pro $0.50/1K, Business $0.35/1K

v1.0.50February 24, 2026

Release highlights

5 updates in this release, covering 5 additions.

Added5
Improved0
Fixed0
  • Added

    Stripe payments

    upgrade, downgrade, and manage billing from the dashboard with Stripe checkout and a billing portal

  • Added

    File attachments

    attach files to any email send

  • Added

    SMTP relay

    send through Sendry using any SMTP-compatible client or library

  • Added

    Inbound email

    receive and parse inbound emails with configurable routing

  • Added

    Email templates

    create reusable HTML templates from the dashboard

At a glance

Version 2.10.3 shipped with a strong focus on product momentum

The newest release includes 0 additions, 0 improvements, and 1 fixes. That makes it a good snapshot of where the platform is heading right now.