> ## Documentation Index
> Fetch the complete documentation index at: https://docs.senderkit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Notable changes to SenderKit's client libraries.

This page tracks releases of SenderKit's developer libraries — the
[TypeScript SDK](/sdks/typescript), the [PHP SDK](/sdks/php), the
[CLI](/cli/installation), and the [MCP server](/mcp/overview).

<Note>
  The SenderKit API and dashboard are still pre-launch. Only the client
  libraries have a public release history today, so that's all this page
  documents. New entries land here as each library ships a version.
</Note>

<Update label="July 8, 2026" description="Message engagement: opens and clicks" tags={["SDK", "CLI", "Webhooks"]}>
  ## Provider-reported opens and clicks on messages

  SenderKit now surfaces provider-reported email **opens** and **link clicks** on
  messages. Both are read-only and set once, on the first occurrence — later
  opens/clicks never update them.

  * `Message` gains **`openedAt`** and **`clickedAt`** — ISO 8601 strings, or
    `null` until the event happens — returned by `messages.get` and
    `messages.list`.
  * `senderkit messages get` prints both fields.
  * Two new subscribable [webhook](/webhooks) events: **`message.opened`** and
    **`message.clicked`**. These are engagement signals only — they never change
    a message's `status` (`delivered` stays the terminal state). The
    `message.clicked` payload additionally carries the clicked `link`.

  This ships as a minor version bump — `@senderkit/sdk@0.13.0` /
  `@senderkit/cli@0.8.0` — in the next release. The Python and PHP SDKs receive
  the equivalent `openedAt`/`clickedAt` fields in their own upcoming releases.
</Update>

<Update label="July 8, 2026" description="v0.12.0 — per-send From overrides on every email send" tags={["SDK", "CLI", "MCP"]}>
  ## Per-message From overrides, now identical on templated and raw sends

  **`@senderkit/sdk@0.12.0` / `@senderkit/cli@0.7.0`**

  Email sends can now override both parts of the From header, on **both**
  `send()` (templated) and `sendRaw()` (raw):

  * **`from`** — From address override (bare address). Previously accepted on
    `sendRaw()` only; now also accepted on `send()`.
  * **`fromName`** — new on both — From display name override, rendered as
    `Name <address>`. Max 128 characters; no control characters or angle
    brackets.

  Either can be set on its own; both fall back to the provider connection's
  configured values. On managed sending, the `from` address is honored only on
  the workspace's verified sending domain, while `fromName` always applies.

  * **CLI:** `senderkit send` and `senderkit send-raw` gain `--from` /
    `--from-name`.
  * **MCP:** `senderkit_send` and `senderkit_send_raw` gain matching `from` /
    `fromName` inputs.

  The Python and PHP SDKs receive the equivalent fields in their own releases.
</Update>

<Update label="July 6, 2026" description="v0.11.0 — `Message.blockedReason` removed from the SDK type" tags={["SDK", "CLI"]}>
  ## `blockedReason` removed from the SDK type

  **`@senderkit/sdk@0.11.0` / `@senderkit/cli@0.6.4`**

  The [June 19 entry](#june-19-2026) below noted that `blockedReason` was retained on
  the `Message` type but never populated by the customer API. As of this release the
  property is removed from the type entirely — referencing `message.blockedReason` in
  TypeScript is now a compile error rather than an always-`undefined` runtime value.

  Nothing else changes: `blocked` remains a valid `Message.status` value, and
  `messages.list({ status: "blocked" })` / `--status blocked` /
  `senderkit_messages_list` still accept it as a filter. If you already branch on
  `status === "blocked"` you're unaffected; if you referenced `blockedReason`
  anywhere, remove that reference.
</Update>

<Update label="July 6, 2026" description="Inbound hooks: retried deliveries no longer send duplicate emails" tags={["Integration"]}>
  ## Inbound webhook retries are now deduplicated

  Sources that deliver to a SenderKit inbound hook endpoint — including the
  [Supabase auth email hook](/guides/supabase-auth-emails) — retry a delivery
  whenever they don't receive a prompt `2xx`, including after transient network
  errors. Previously, a retried delivery could enqueue the underlying email a
  second time, so a recipient occasionally received a duplicate confirmation,
  magic-link, or recovery email.

  Retried deliveries of the same event are now recognized and deduplicated
  automatically — a retry resolves without sending again. No configuration changes
  are needed.
</Update>

<Update label="June 26, 2026" description="Logs: preview the rendered content of any message" tags={["Dashboard"]}>
  ## Message preview in Logs

  The **Dashboard → Logs** detail drawer now includes a **Preview** action — click
  the kebab menu on any selected message to see what was actually sent:

  * **Email** — renders the HTML in a sandboxed iframe (no remote resources, tracking
    pixels, or external fonts load inside the preview).
  * **SMS / push / web-push** — shows the message body as plain text.

  The preview is fetched on demand when you open it, so the logs list loads as fast
  as before.

  **Fidelity note:** if the template uses a shared reusable block, the preview renders
  the block's *current* content rather than a snapshot of what it was at send time.
  The drawer flags this with an **"approximate"** badge so you know the preview may
  differ slightly from what the recipient received.
</Update>

<Update label="June 19, 2026" description="blockedReason is operator-only — not returned via customer API" tags={["Dashboard"]}>
  ## `blockedReason` is operator-only

  The `blockedReason` field is not returned in any customer-facing message read
  (`messages.list`, `messages.get`, the SDK, the SSE tail, or the Logs page).

  When the outbound abuse scanner halts a send, the customer-visible message timeline
  records only a generic notice: **"Blocked by automated content safety checks."**
  The detailed signal breakdown (which heuristic fired, LLM confidence score, etc.)
  is kept operator-only in the admin console and is intentionally not disclosed to
  senders — surfacing detection signals would help bad actors evade them.

  The `Message.blockedReason` property in the SDK type (`string | null | undefined`)
  is retained as an optional field but is never populated in customer API responses.
  (Update: as of the [July 6, 2026 entry](#july-6-2026) above, the property has been
  removed from the SDK type entirely.)
  The earlier June 17 changelog entry described `blockedReason` as containing
  human-readable detail (e.g. `"High-confidence phishing content detected"`) — that
  description was incorrect and has been superseded by this entry.

  ### What still works

  * `messages.list({ status: "blocked" })` — filtering for blocked messages works as
    documented. Only `blockedReason` is withheld; every other message field is present.
  * `Message.status === "blocked"` — detection and the status transition are unchanged.
</Update>

<Update label="June 19, 2026" description="Logs: scroll back through full message history" tags={["Dashboard"]}>
  ## Load older message history in Logs

  The **Dashboard → Logs** page previously showed only the live tail — a streaming
  view of your most recent sends with no way to page backward to investigate
  earlier messages.

  Older history now loads beneath the live tail as one continuous list:

  * **Scroll to load** — as you scroll toward the bottom, the next 50 messages load
    automatically. A manual **"Load older messages"** button is available as a
    fallback.
  * **Live updates while reading back** — when you're scrolled away from the top,
    incoming live messages are buffered behind a **"↑ N new messages"** pill instead
    of shifting the viewport. Returning to the top flushes the buffer and resumes
    the live tail.
  * **"Beginning of history"** footer appears once all available records have loaded.
  * The client-side text filter applies to all loaded messages combined.

  The underlying API (`GET /v1/messages?cursor=…`) is unchanged — the dashboard now
  surfaces its pagination to you directly.
</Update>

<Update label="June 17, 2026" description="v0.10.0 — blocked message status, outbound abuse detection" tags={["SDK", "CLI"]}>
  ## `blocked` message status

  **`@senderkit/sdk@0.10.0` / `@senderkit/cli@0.6.3`**

  SenderKit now runs outbound anti-phishing detection over email and SMS content
  before handing a message to a provider. A flagged send is halted and the message
  lands in a new terminal `blocked` status.

  ### What's new

  * **`blocked` status** — `Message.status` can now be `"blocked"`. A blocked message
    was stopped by the abuse scanner before provider dispatch and will not be retried.
    The message timeline records a generic notice; detection details are operator-only
    (see the [June 19 entry](#june-19-2026) for clarification).
  * **`messages.list({ status: "blocked" })`** — the status filter now accepts
    `"blocked"` in the SDK, CLI (`--status blocked`), and MCP
    (`senderkit_messages_list`). Before this release, filtering by `"blocked"`
    was rejected as an invalid enum value.

  The synchronous send response is unchanged — a blocked message still starts as
  `queued`; the abort happens asynchronously during content scanning.

  See [Messages → lifecycle](/concepts/messages#the-message-lifecycle) for the full
  status reference.
</Update>

<Update label="June 16, 2026" description="Starter plan, updated pricing" tags={["Dashboard", "Billing"]}>
  ## Starter plan + pricing restructure

  SenderKit's pricing now has four tiers:

  | Plan    | Price   | Templates | Messages/month | Custom domain |
  | ------- | ------- | --------- | -------------- | ------------- |
  | Free    | \$0     | 10        | 500            | No            |
  | Starter | \$5/mo  | Unlimited | 5,000          | Yes           |
  | Pro     | \$19/mo | Unlimited | 50,000         | Yes           |
  | Custom  | Contact | Unlimited | Unlimited      | Yes           |

  ### What changed

  * **Free** — template cap raised from 5 → **10**; message cap lowered from 3,000 →
    **500**/month. The free tier is now scoped for evaluation and low-volume personal
    projects rather than production transactional mail.
  * **Starter** ✨ **new** — \$5/month: unlimited templates, 5,000 messages/month,
    and access to **custom sending domains** for DKIM alignment.
  * **Pro** — now has an enforced **50,000 messages/month** cap (was uncapped). Price
    and unlimited templates are unchanged.
  * **Custom sending domains** are now available on **any paid plan** (Starter and
    Pro), not just Pro. See
    [Channels → Custom sending domains](/concepts/channels-and-providers#custom-sending-domains).

  ### Built-in email sender caps

  The built-in `@tx.senderkit.email` sender enforces caps to protect the shared
  domain's reputation. These caps are **separate from and lower than** the plan's
  overall message quota — verifying a custom sending domain unlocks the higher tier:

  | Plan    | Shared `@tx.senderkit.email` | Verified custom domain  |
  | ------- | ---------------------------- | ----------------------- |
  | Free    | 500/month, 50/day            | —                       |
  | Starter | 1,500/month, 150/day         | 5,000/month, 500/day    |
  | Pro     | 15,000/month, 1,500/day      | 50,000/month, 5,000/day |

  Connecting your own BYOP email provider (Postmark, SES, etc.) routes through that
  provider instead and bypasses the built-in caps entirely.

  See [Channels → Built-in email sender](/concepts/channels-and-providers#built-in-email-sender)
  for full details.

  ### Upgrading

  Existing free workspaces are not automatically migrated — you keep your current
  limits until you choose to upgrade. Select a plan from **Dashboard → Billing**.
</Update>

<Update label="June 14, 2026" description="Supabase auth-email integration" tags={["Dashboard", "Integration"]}>
  ## Route Supabase auth emails through SenderKit

  A new **Integrations** page in the SenderKit dashboard lets you connect your
  Supabase project and route all of its auth emails through SenderKit in one click —
  sign-up confirmation, magic link, password reset, invite, email change, and
  reauthentication.

  ### One-click setup

  Go to **Dashboard → Integrations → Connect Supabase**. Authorize SenderKit, pick
  a project, and SenderKit automatically:

  * Provisions a signed inbound webhook endpoint
  * Seeds and publishes six curated auth templates (`supabase-signup`,
    `supabase-magiclink`, `supabase-recovery`, `supabase-invite`,
    `supabase-email-change`, `supabase-reauthentication`)
  * Configures Supabase's Send Email Hook to point at the endpoint

  After connecting, customize the seeded templates in **Templates** to match your
  brand. Verify a custom sending domain under **Channels → Domains** for DKIM
  alignment and best inbox placement.

  See the [Supabase auth emails guide](/guides/supabase-auth-emails) for template
  variable reference, limits, and troubleshooting.
</Update>

<Update label="June 13, 2026" description="MCP tool manifest improvements" tags={["SDK", "CLI"]}>
  ## MCP tool manifest sync

  Minor improvements to the tool descriptions in `@senderkit/sdk` and the
  CLI-bundled MCP server, keeping them in sync with the hosted endpoint at
  `mcp.senderkit.com`:

  * **`senderkit_send.to`** now explicitly covers all four channels: email address,
    E.164 phone number, push device token, and JSON-encoded web-push
    `PushSubscription`.
  * **`senderkit_messages_list.status`** is now a strict enum in the tool schema
    (`scheduled`, `queued`, `rendered`, `dispatched`, `sent`, `delivered`, `failed`,
    `opted_out`, `canceled`), matching the API's behavior of rejecting unknown statuses
    with `400`. The CLI validates `--status` client-side and lists choices in `--help`.
  * **Template slugs** are documented as always lowercase in the `senderkit_send`
    `template` parameter and `senderkit_templates_get` `slug` parameter. Slugs are
    canonicalized to lowercase on every write path (existing mixed-case slugs continue
    to resolve, but new slugs are created lowercase).
</Update>

<Update label="June 12, 2026" description="PHP SDK v0.2.0 — core, Laravel, and Symfony" tags={["SDK"]}>
  ## PHP SDK: `senderkit/senderkit-php` v0.2.0

  The official PHP SDK is now available. It ships as three Composer packages:

  * **`senderkit/senderkit-php`** — framework-agnostic core (PSR-18 HTTP client,
    PHP 8.1+).
  * **`senderkit/senderkit-laravel`** — service provider, `SenderKit` facade,
    `senderkit` notification channel (all four channels), `senderkit` mail
    transport for existing Mailables, and `VerifyWebhookSignature` middleware.
  * **`senderkit/senderkit-symfony`** — bundle with autowiring and a
    `RequestVerifier` webhook helper.

  ```bash theme={null}
  composer require senderkit/senderkit-php       # core
  composer require senderkit/senderkit-laravel   # Laravel
  composer require senderkit/senderkit-symfony   # Symfony
  ```

  The `Client` class mirrors the TypeScript SDK: `send()`, `sendRaw()`,
  `sendBatch()`, `context()`, `messages->*`, and `templates->*`. A PSR-18
  HTTP client (Guzzle or `symfony/http-client`) is auto-discovered or can be
  injected.

  ```php theme={null}
  use SenderKit\Client;
  use SenderKit\Request\TemplateSend;

  $sk = new Client(apiKey: getenv('SENDERKIT_API_KEY'));
  $result = $sk->send(new TemplateSend('welcome', 'user@example.com', ['name' => 'Ada']));
  echo $result->id; // msg_…
  ```

  See the [PHP SDK reference](/sdks/php) for the full API, Laravel notification
  channel, mail transport, and Symfony bundle setup.
</Update>

<Update label="June 11, 2026" description="Custom sending domains (Pro)" tags={["Dashboard"]}>
  ## Custom sending domains

  Pro workspaces can now connect their own sending domains and send from any
  address on a verified domain — without the shared `@tx.senderkit.email` address
  appearing to recipients.

  **How it works:**

  * Add a domain in **Dashboard → Channels → Add domain** (Pro-gated; free
    workspaces see an upgrade prompt).
  * Publish four DNS records: a white-label DKIM TXT record at
    `senderkit._domainkey.{domain}`, an MX + SPF pair on `send.{domain}` for the
    custom MAIL FROM, and optionally a root SPF and DMARC record. The dashboard
    shows the exact values to copy and probes DNS live once records are detected.
  * SenderKit verifies the domain (polls every 5 minutes; **Check now** button for
    immediate re-check). Verification completes within minutes once records
    propagate; claims expire after 72 hours.
  * Once verified, the domain activates as a **managed connection** in Channels.
    It can be set as the workspace default or targeted per template. The shared
    `tx.senderkit.email` sender stays as the implicit fallback.

  **From-address override** — raw sends (`POST /v1/send-raw`, `senderkit send-raw`,
  `senderkit_send_raw`) now honor a `from` address whose domain exactly matches a
  verified custom domain:

  ```ts theme={null}
  await senderkit.sendRaw({
    channel: "email",
    to: "customer@example.com",
    from: "billing@acme.com",   // acme.com must be verified
    content: { subject: "Your invoice", html: "…" },
  });
  ```

  Custom-domain connections have their own per-connection cap tier and billing
  meter, separate from the built-in shared sender caps.

  See [Channels → Custom sending domains](/concepts/channels-and-providers#custom-sending-domains).
</Update>

<Update label="June 11, 2026" description="v0.8.0 — API key scopes enforced, SenderKitPermissionError" tags={["SDK", "CLI"]}>
  ## Least-privilege API key scopes

  API keys now carry an enforced scope set. Previously, scopes were stored but
  never checked — every valid key had full access. Starting with this release, a
  scoped key used outside its grant returns `403` with `code: "insufficient_scope"`.

  ### Scope model

  Three scopes: `read` · `send` · `cancel`. All three are granted by default when
  creating a key; the dashboard scope picker lets you restrict to fewer. Keys minted
  without explicit scopes remain **unscoped** (full access) — no migration needed,
  no existing integrations break.

  | Scope    | Covers                                                          |
  | -------- | --------------------------------------------------------------- |
  | `read`   | Messages, templates, context reads                              |
  | `send`   | `POST /v1/send`, `POST /v1/send-raw`, draft template operations |
  | `cancel` | `DELETE /v1/messages/{id}`                                      |

  ### TypeScript SDK (`@senderkit/sdk` v0.8.0)

  **New `SenderKitPermissionError`** — `403` responses now throw
  `SenderKitPermissionError` instead of the previous `SenderKitAuthenticationError`.
  Because it extends `SenderKitApiError`, existing `catch (SenderKitApiError)` blocks
  still handle it; add a specific `SenderKitPermissionError` branch to distinguish
  scope errors from auth errors:

  ```ts theme={null}
  import {
    SenderKitPermissionError,
    SenderKitAuthenticationError,
  } from "@senderkit/sdk";

  try {
    await senderkit.send(…);
  } catch (err) {
    if (err instanceof SenderKitPermissionError) {
      // Key is valid but lacks the required scope
    } else if (err instanceof SenderKitAuthenticationError) {
      // Key is bad/missing/revoked
    }
  }
  ```

  **New `ApiScope` type** — `"read" | "send" | "cancel"`, exported from
  `@senderkit/sdk`.

  ### CLI (`@senderkit/cli` v0.6.1)

  A `403` is now reported as **"Permission denied: … The API key is valid but
  lacks the required scope."** (JSON: `type: "permission"`) instead of the
  previous misleading "Authentication failed" message.

  See [Authentication → Scopes](/authentication#scopes) and the
  [SDK error reference](/sdks/typescript#error-handling).
</Update>

<Update label="June 8, 2026" description="CLI: senderkit mcp install defaults to hosted OAuth" tags={["CLI"]}>
  ## `senderkit mcp install` now defaults to hosted OAuth

  `senderkit mcp install` now writes a **hosted endpoint config** (`url`-only,
  pointing at `https://mcp.senderkit.com`) with OAuth by default — no API key is
  stored on disk and your MCP client handles sign-in. Previously the default was a
  local stdio subprocess with an API key.

  ```bash theme={null}
  # Hosted OAuth config for every detected client (new default — no API key stored)
  senderkit mcp install

  # Hosted with API key bearer auth instead of OAuth
  senderkit mcp install --api-key-auth

  # Local stdio subprocess (previous default, now opt-in)
  senderkit mcp install --local
  ```

  Codex users get a native streamable-HTTP `url` in `config.toml` (no longer
  bridged through `mcp-remote`), and can sign in with `codex mcp login senderkit`.

  The `--remote` flag is kept as a no-op alias for back-compat (it now just
  selects the new default behavior). The `--local` + `--url` combination is
  rejected with a clear error.
</Update>

<Update label="June 8, 2026" description="v0.7.0 — lean template reads, TemplateVersion.content removed" tags={["SDK"]}>
  ## Lean template and message reads (`TemplateVersion.content` removed)

  **`@senderkit/sdk@0.7.0` / `@senderkit/cli@0.5.1`**

  `templates.get()` no longer includes a `content` field in the response — it
  was already `undefined` at runtime after the server stopped returning the raw
  template body. This release makes the TypeScript types reflect reality.

  ### What changed

  * **`TemplateVersion.content` is removed** from the `Template` type. Code that
    accessed `template.currentVersion.content` will now produce a TypeScript compile
    error. The rest of `currentVersion` (`versionNumber`, `variables`, `publishedAt`)
    is unchanged.
  * `messages.list()` and `messages.get()` also omit rendered content; `vars`,
    `timeline`, and `metadata` are still returned on every message.

  **No CLI behavior change** — `senderkit templates get` only surfaced version
  metadata, never raw content.
</Update>

<Update label="June 7, 2026" description="MCP template generation tools" tags={["MCP"]}>
  ## AI-powered template creation via MCP

  Two new tools on the **hosted MCP endpoint** let an AI agent author SenderKit
  templates from a plain-language brief, without the user opening the dashboard.

  ### `senderkit_templates_create`

  Generate a new template from a brief and save it as a **draft**. The tool
  composes channel-native content server-side and returns a deep link to the
  editor for review. Nothing is sent or published until the user approves the
  draft. Enforces the workspace's template cap before the AI call runs.

  ### `senderkit_templates_regenerate`

  Fully rewrite an existing **draft** template from a new brief, reusing any
  already-declared variables. Only draft templates can be regenerated — published
  templates are left untouched.

  Both tools are available on the **hosted endpoint** (`mcp.senderkit.com`) and
  via OAuth; they are not available on the CLI-bundled stdio server because they
  require server-side AI generation. See the [Tools reference](/mcp/tools) for
  parameters.
</Update>

<Update label="June 5, 2026" description="v0.4.0 — senderkit_context, OAuth MCP, email deliverability" tags={["SDK", "CLI", "MCP"]}>
  ## senderkit\_context tool + mode on send results

  ### TypeScript SDK

  No SDK-visible changes in this release.

  ### CLI

  New `senderkit context` command reports the active connection's live/test mode.
  Useful in scripts and in MCP sessions to confirm which key is active before
  sending.

  ```bash theme={null}
  senderkit context
  # mode:     test
  # livemode: false
  ```

  ### MCP server

  **New `senderkit_context` tool** — returns `{ workspaceId, mode, livemode }`.
  Call it before sending to confirm whether the connection is live (real
  delivery) or test (recorded only). Available on both the CLI stdio server and
  the hosted endpoint.

  **`mode` field on send results** — `senderkit_send` and `senderkit_send_raw`
  results now include a `mode` field alongside the message `id` and `status`,
  giving the model a post-send confirmation of which mode the message was
  dispatched in.

  **Tool titles and behaviour annotations** — every tool now exposes a
  human-readable `title` and a behaviour hint (`readOnlyHint` or
  `destructiveHint`) per the MCP protocol. These appear in compatible client
  UIs and are required for the Anthropic Claude Connectors Directory.

  **OAuth connection for claude.ai** — `mcp.senderkit.com` now acts as a full
  OAuth 2.1 authorization server, so claude.ai and other OAuth-only MCP clients
  can connect without a manual API key. Sign in with your SenderKit account and
  pick a workspace + mode on the consent screen. See
  [Installation → Connect from claude.ai](/mcp/installation#connect-from-claude-ai-oauth).
</Update>

<Update label="June 6, 2026" description="v0.6.0 — workspace identity & client.context()" tags={["SDK", "CLI", "MCP"]}>
  ## Workspace identity in context

  ### TypeScript SDK

  **New `client.context()` method** — calls `GET /v1/context` and returns a
  `SenderKitContext` (`{ workspace: { id, slug, name }, mode }`). Use it to confirm
  which workspace an API key belongs to before sending:

  ```ts theme={null}
  const ctx = await senderkit.context();
  console.log(ctx.workspace.name); // "Acme Inc"
  console.log(ctx.mode);           // "live"
  ```

  `SenderKitContext` is now a named export alongside the existing types.

  ### CLI

  `senderkit context` now calls `GET /v1/context` (was a local key-prefix check)
  and includes the workspace name and slug in its output:

  ```bash theme={null}
  senderkit context
  # workspace: Acme Inc
  # slug:      acme
  # mode:      live
  ```

  `--json` returns `{ workspace: { id, slug, name }, mode }`.

  ### MCP server

  `senderkit_context` now returns `{ workspace: { id, slug, name }, mode }`.
  The `workspaceId` and `livemode` fields from v0.4.0 are replaced by the
  structured `workspace` object and a single `mode` string.
</Update>

<Update label="June 3, 2026" description="v0.3.1 — default URL corrections" tags={["SDK", "CLI"]}>
  ## Default URL corrections

  ### TypeScript SDK

  `DEFAULT_BASE_URL` changed from `https://senderkit.com/api` to
  `https://api.senderkit.com`. The SDK now targets `https://api.senderkit.com/v1/send`
  (and sibling routes) by default, matching the OpenAPI spec's canonical server. The
  previous apex URL triggered an apex→www redirect that stripped the `Authorization`
  header, causing 401s. Callers passing an explicit `baseUrl` are unaffected.

  ### CLI

  The hosted MCP default corrected from `https://mcp.senderkit.com/mcp` to
  `https://mcp.senderkit.com`. On the dedicated MCP host the subdomain root is
  the endpoint — any other path (including `/mcp`) returns 404.
</Update>

<Update label="June 2, 2026" description="v0.3.0 — web-push channel" tags={["SDK", "CLI", "MCP"]}>
  ## Web Push channel

  `web-push` (browser notifications via the Web Push Protocol + VAPID) is now a
  first-class channel, distinct from the mobile `push` channel.

  ### TypeScript SDK

  * `Channel` now includes `"web-push"`.
  * New `RawWebPushContent` type: `{ title, body, icon?, clickUrl?, badge?, data? }`.
  * `sendRaw({ channel: "web-push", to, content })` — `to` is the JSON-encoded
    browser `PushSubscription` (endpoint + `p256dh`/`auth` keys). Your application
    collects these after the user grants browser permission.

  ### CLI

  * `senderkit send-raw --channel web-push` with `--title`, `--body`, `--icon`,
    `--click-url`, `--badge`, and `--push-data` flags.

  ### MCP server

  * `senderkit_send_raw` and `senderkit_messages_list` `channel` enums now accept
    `"web-push"`. The `icon` and `clickUrl` inputs are available on `senderkit_send_raw`.
    Both the CLI stdio server and the app-hosted HTTP MCP server are updated.

  ### Dashboard / API

  The `web-push` channel is also fully supported in the SenderKit dashboard and REST
  API: connect a VAPID keypair, create web-push templates, and send via
  `POST /v1/send` or `POST /v1/send-raw`.
</Update>

<Update label="May 31, 2026" description="v0.2.1 — CLI email-envelope flags" tags={["SDK", "CLI"]}>
  ## CLI cc/bcc/attachments flags fixed

  `--cc`, `--bcc`, and `--attachments` flags on `senderkit send` and
  `senderkit send-raw` were advertised in `--help` but threw a Zod validation
  error at runtime — the CLI adapter passed them as strings while the validator
  expected arrays or objects. They now work correctly:

  * **`--cc` / `--bcc`** — accept a comma-separated string (`a@x.com,b@x.com`)
    or a JSON array string (`'["a@x.com","b@x.com"]'`).
  * **`--attachments`** — accepts a JSON array string of attachment objects.

  The MCP server path (real arrays/objects) is unchanged.

  Also fixes the SDK's `VERSION` constant and the CLI's `--version` output,
  which were reporting stale values. Both are now derived directly from
  `package.json` at build time and cannot drift.
</Update>

<Update label="May 28, 2026" description="v0.2.0 — initial public release" tags={["SDK", "CLI", "MCP"]}>
  ## Initial public release

  The first public release of the SenderKit developer libraries. All three
  libraries are versioned together and published to npm at `0.2.0`.

  ### TypeScript SDK — `@senderkit/sdk@0.2.0`

  * `SenderKit` client with `send()`, `sendRaw()`, and `sendBatch()` for
    multi-channel delivery (email, SMS, push).
  * `messages.list()` (with metadata filtering), `messages.get(id)`, and
    `messages.cancel(id)`.
  * `templates.list()` and `templates.get(slug)`.
  * `scheduledAt` on `send` / `sendRaw` for scheduled delivery.
  * `client.mode` (`"live"` / `"test"`), derived from the API key prefix; the
    client validates that keys start with `sk_live_` or `sk_test_` at
    construction.
  * Typed error hierarchy: `SenderKitApiError`,
    `SenderKitAuthenticationError`, `SenderKitValidationError`,
    `SenderKitRateLimitError`, `SenderKitTimeoutError`, and
    `SenderKitNetworkError`.

  See the [SDK reference](/sdks/typescript) for full usage.

  ### CLI — `@senderkit/cli@0.2.0`

  * `senderkit login` for interactive authentication (API key input is hidden).
  * `senderkit send` and `senderkit send-raw`.
  * `senderkit templates list` / `get`.
  * `senderkit messages list` / `get` / `cancel`.
  * `--json` flag for structured, machine-readable output (including errors).

  See the [CLI docs](/cli/installation) to get started.

  ### MCP server — bundled in `@senderkit/cli@0.2.0`

  Exposes SenderKit to AI agents over the Model Context Protocol, via the
  `senderkit mcp` command (stdio and HTTP transports). Tools:

  * `senderkit_send` and `senderkit_send_raw`
  * `senderkit_templates_list` and `senderkit_templates_get`
  * `senderkit_messages_list`, `senderkit_messages_get`, and
    `senderkit_cancel_message`

  See the [MCP overview](/mcp/overview) and [tools reference](/mcp/tools).
</Update>
