> ## 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="August 23, 2026" description="API/Logs: search messages by id, recipient, template, or metadata" tags={["Dashboard"]}>
  ## Full-text search over your message history

  `GET /v1/messages` (and the dashboard's **Logs** page) now accepts a
  `search` query param: a case-insensitive substring match across the
  message id, recipient, template, and your `metadata` keys/values. It
  composes with the existing `status`/`channel`/`template`/`metadata`
  filters and cursor pagination, and is capped at 512 characters. See
  [Querying messages](/concepts/messages#querying-messages).
</Update>

<Update label="August 19, 2026" description="@senderkit/sdk@0.16.0 — MCP output schemas and structured results" tags={["SDK", "MCP"]}>
  ## SDK/MCP release: an `outputSchema` on every tool

  **`@senderkit/sdk@0.16.0`**

  * **Every MCP manifest tool now declares an `outputSchema`**
    (`McpToolSpec.outputSchema`, plus one exported `*Output` Zod shape per
    tool in `@senderkit/sdk/mcp`) describing its structured result. The
    hosted server at `mcp.senderkit.com` advertises these via `tools/list`
    and returns every result as `structuredContent` alongside the existing
    JSON text. See [Structured results](/mcp/tools#structured-results).
  * **Message results no longer include internal identifiers** on the hosted
    MCP server: `senderkit_messages_list` and `senderkit_messages_get` return
    the public `publicId` but not the database row id, workspace id, or
    provider-connection id. The REST API's `GET /v1/messages` responses are
    unchanged.
  * Exports the `TEMPLATE_STATUSES`, `INBOUND_MESSAGE_STATUSES`,
    `INBOUND_DOMAIN_KINDS`, and `INBOUND_DOMAIN_STATUSES` enums the shapes
    use.
  * The CLI-bundled stdio server is unchanged in this release (it does not
    declare output schemas yet).
</Update>

<Update label="August 17, 2026" description="Mailgun: temporary delivery delays no longer marked as failed" tags={["Integration"]}>
  ## Mailgun connections no longer strand messages on a temporary delay

  A Mailgun connection could previously mark a message `failed` (and fire a
  `message.failed` [webhook](/webhooks)) for what was actually a temporary
  delivery delay — greylisting or a transient rejection the receiving mail
  server was still going to retry. If Mailgun went on to deliver the message
  anyway, that later delivery could no longer update the message's status, so
  it stayed stuck at `failed` despite having arrived. Only a genuinely
  permanent failure is now treated as terminal; a temporary delay leaves the
  message in its prior status until a real `delivered` or `failed` outcome
  arrives. No action is needed on your end.
</Update>

<Update label="August 14, 2026" description="Twilio: SMS delivery status now updates" tags={["Integration"]}>
  ## Twilio SMS delivery status tracking now works

  Twilio SMS connections could never record delivery status — messages stayed
  at `sent`, and the `message.delivered` / `message.failed`
  [webhooks](/webhooks) never fired for SMS sends, regardless of how the
  connection was configured. This is fixed.

  Delivery status tracking for a Twilio connection is opt-in: set the
  connection's **Status callback URL (optional)** field to the exact
  status-callback URL configured in Twilio for that number or Messaging
  Service (Twilio signs it verbatim, so it must match character-for-character,
  including any trailing slash). Sends work the same either way — this field
  only affects whether status updates come back.
</Update>

<Update label="August 13, 2026" description="@senderkit/sdk@0.15.0 / @senderkit/cli@0.10.0 — complete MCP safety-hint trio" tags={["SDK", "CLI", "MCP"]}>
  ## SDK/CLI/MCP release: explicit safety hints on every tool

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

  * **Every MCP tool now states all three safety hints explicitly** —
    `readOnlyHint`, `openWorldHint`, and `destructiveHint` — instead of
    leaving some to a client's inferred defaults.
    [`senderkit_send`](/mcp/tools#senderkit_send) and
    [`senderkit_send_raw`](/mcp/tools#senderkit_send_raw) are the only tools
    with `openWorldHint: true`, since they're the only ones that deliver
    messages to recipients outside SenderKit; every read-only tool now also
    states `openWorldHint: false` and `destructiveHint: false` directly.

      <Note>
        For TypeScript consumers, this widens the SDK's `ToolAnnotations` type:
        code that previously constructed a partial value (e.g.
        `{ readOnlyHint: true }`) must now supply all three hints.
      </Note>

  * The inbound message [`verdicts`](/concepts/inbound#reading-received-mail)
    field description is now provider-neutral — it describes scanning
    verdicts (spam/virus/SPF/DKIM) without naming the underlying receiving
    infrastructure. The field's shape and runtime values are unchanged.

  * [`senderkit_send_raw`](/mcp/tools#senderkit_send_raw)'s `scheduledAt`
    field documents the same "must be in the future, within 30 days"
    constraint as `senderkit_send`, and its `subject`/`html` fields now state
    they're required when `channel` is `"email"`.

  The Python and PHP SDKs are unaffected — these are MCP-manifest-only changes.
</Update>

<Update label="August 6, 2026" description="@senderkit/sdk@0.14.2 / @senderkit/cli@0.9.2 — SMS recipient format documentation" tags={["SDK", "CLI", "MCP"]}>
  ## SDK/CLI/MCP release: SMS recipient format

  **`@senderkit/sdk@0.14.2` / `@senderkit/cli@0.9.2`**

  The hosted API rejects a non-E.164 SMS `to` up front with `400 invalid_recipient`
  — for both template and raw sends — instead of accepting it and failing later at
  dispatch. This release brings the SDK/CLI/MCP surface in line with that contract:

  * **[`senderkit_send_raw`](/mcp/tools#senderkit_send_raw)'s `to` field** now
    spells out the per-channel recipient formats, including the E.164
    requirement for `sms` — matching [`senderkit_send`](/mcp/tools#senderkit_send),
    which already documented it. This flows through to the `senderkit send-raw`
    CLI help.
  * The SDK README's SMS example now notes the E.164 requirement and the
    `400 invalid_recipient` response.

  No behavior change in the client itself — recipient validation stays
  server-side. The Python and PHP SDKs are unaffected.
</Update>

<Update label="August 1, 2026" description="@senderkit/sdk@0.14.1 / @senderkit/cli@0.9.1 — inbound tool wording and annotation fixes" tags={["SDK", "CLI", "MCP"]}>
  ## SDK/CLI/MCP release: inbound tool corrections

  **`@senderkit/sdk@0.14.1` / `@senderkit/cli@0.9.1`**

  A follow-up pass on the inbound email tools in the MCP manifest, now also
  the single source powering the hosted MCP server's inbound tool suite:

  * **[`senderkit_inbound_addresses_create`](/mcp/tools#senderkit_inbound_addresses_create)
    is annotated `destructiveHint: false`** — creating an address is additive
    and fully reversed by deleting it, so well-behaved clients no longer
    prompt for confirmation on it.
  * **`localPart` documents the real format**: 1-64 characters of `a-z`,
    `0-9`, `.`, `_`, `-`, must start and end alphanumeric, lowercased
    automatically, some names reserved — plus the `"*"` catch-all shorthand.
  * **`webhookEndpointId` documents the full binding behavior**: a bound
    endpoint receives `message.received` even if it isn't itself subscribed
    to that event, but it must be active and share the address's `livemode`;
    left unset, the event fans out to every active subscribed endpoint in the
    address's mode.
  * **`livemode` no longer implies a quota exemption.** The field now states
    plainly that every inbound address, test or live, counts toward the
    plan's inbound-address limit, and that a test-mode address's `forwardTo`
    is recorded as a test send rather than actually delivered.
  * [`senderkit_inbound_messages_list`](/mcp/tools#senderkit_inbound_messages_list)
    and [`senderkit_inbound_messages_get`](/mcp/tools#senderkit_inbound_messages_get)
    were retitled "List Inbound Messages" / "Get Inbound Message" for
    consistency with the rest of the tool suite.

  The Python and PHP SDKs are unaffected — these are MCP-manifest-only changes.
</Update>

<Update label="July 31, 2026" description="@senderkit/sdk@0.14.0 / @senderkit/cli@0.9.0 — MCP manifest wire-contract fixes" tags={["SDK", "CLI", "MCP"]}>
  ## SDK/CLI/MCP release: manifest wire-contract fixes

  **`@senderkit/sdk@0.14.0` / `@senderkit/cli@0.9.0`**

  A pass over the MCP tool manifest tightened several tool schemas to state
  the real wire contract:

  * **`cc` / `bcc` are documented as JSON arrays, capped at 50 recipients.**
    [`senderkit_send`](/mcp/tools#senderkit_send) and
    [`senderkit_send_raw`](/mcp/tools#senderkit_send_raw) now say so directly,
    matching the API's existing limit. The CLI keeps accepting comma-separated
    values via its own flag help — nothing changes for CLI users.
  * **List limits are enforced in the schema, not just the description.**
    [`senderkit_messages_list.limit`](/mcp/tools#senderkit_messages_list) is
    bounded 1–200 (default 50); [`senderkit_inbound_messages_list.limit`](/mcp/tools#senderkit_inbound_messages_list)
    enforces the 1–100 range its description already claimed. An out-of-range
    value now fails validation instead of being silently clamped server-side.
  * **Additive writes carry an honest annotation.** Tools that only create
    something new — `senderkit_templates_create` and
    `senderkit_inbound_addresses_create` — now declare `destructiveHint: false`
    instead of `true`, so well-behaved MCP clients don't demand confirmation
    for a reversible, non-destructive call.

  The Python and PHP SDKs are unaffected — these are MCP-manifest-only changes.
</Update>

<Update label="July 31, 2026" description="MCP/API: field-specific send errors, E.164 SMS validation, stricter inbound cursor" tags={["MCP"]}>
  ## Clearer send errors, plus stricter recipient and cursor validation

  A few consistency fixes to the hosted MCP server and the REST API it wraps —
  no SDK/CLI version bump required:

  * **Field-specific validation errors.** A malformed
    [`senderkit_send`](/mcp/tools#senderkit_send) /
    [`senderkit_send_raw`](/mcp/tools#senderkit_send_raw) call — e.g. an email
    send missing `subject` or `html` — now names the offending field
    (`invalid_request: subject Required`) instead of a generic `Invalid input`
    message. The equivalent `POST /v1/send` / `POST /v1/send-raw` REST error
    `issues` array gained the same precision.
  * **SMS recipients are validated as E.164.** `to` on an SMS send must now be
    a valid E.164 phone number (e.g. `+14155551234`); previously a malformed
    value was accepted and simulated as delivered instead of being rejected.
  * **`before` on inbound message listing is validated strictly.**
    [`senderkit_inbound_messages_list.before`](/mcp/tools#senderkit_inbound_messages_list)
    and `GET /v1/inbound/messages?before=` now reject a non-ISO-8601 value
    with a clear `invalid_request` error instead of silently returning an
    empty page.
</Update>

<Update label="July 29, 2026" description="@senderkit/sdk@0.13.0 / @senderkit/cli@0.8.0 — inbound namespace, suppressed status, full custom-domain reference" tags={["SDK", "CLI", "MCP"]}>
  ## SDK/CLI/MCP release: inbound namespace + suppressed status type

  **`@senderkit/sdk@0.13.0` / `@senderkit/cli@0.8.0`**

  This release brings the TypeScript SDK, CLI, and MCP server in step with
  three changes already covered by earlier entries on this page:

  * **`client.inbound`** — a new namespace with `addresses` (`list`, `create`,
    `delete`), `messages` (`list`, `get`, `raw`, `attachment`), and `domains`
    (`list`, `create`, `delete`), matching the [Inbound Email](/concepts/inbound)
    API in full — including the custom-domain and catch-all support from the
    July 26 entry below. The CLI gains eight `senderkit inbound *` commands,
    and the MCP server eight matching tools; see the
    [Tools reference](/mcp/tools#inbound) for parameters.
  * **`suppressed`** is now included in the SDK's exported `Message.status`
    values (the status itself was introduced in the July 26 entry below).
  * **`openedAt`** / **`clickedAt`** on `Message` — the fields the July 8
    entry below described ship in this release.

  The Python and PHP SDKs receive the equivalent inbound/suppressed/engagement
  support in their own upcoming releases.
</Update>

<Update label="July 26, 2026" description="Logs: retry a failed message" tags={["Dashboard"]}>
  ## Retry a failed send from the Logs page

  Failed messages in the dashboard's **Logs** view now have a **Retry send**
  action in the row menu. It re-queues the message for another dispatch
  attempt — the status badge flips to `queued` and the message follows the
  normal lifecycle from there, including the usual suppression and content
  checks. Only messages with a `failed` status are retryable.
</Update>

<Update label="July 26, 2026" description="opted_out no longer overwrites a message's delivery outcome" tags={["Webhooks", "Dashboard"]}>
  ## `opted_out` now only reflects sends skipped before dispatch

  A recipient unsubscribing or complaining **after** a message was already
  `delivered` (or `failed`) previously flipped that message's `status` to
  `opted_out`, discarding its actual delivery outcome. `opted_out` now means
  exactly one thing: the send was skipped before dispatch because the
  recipient had already opted out. A delivered message stays `delivered` even
  if the recipient unsubscribes from it afterward.

  * **New `message.complained` [webhook event](/webhooks#events)**, separate
    from `message.opted_out`. A spam complaint now fires both events; a plain
    unsubscribe fires `message.opted_out` alone.
  * `messages.list({ status: "opted_out" })` and the equivalent CLI/MCP status
    filters now match only pre-send suppressions, not post-delivery
    unsubscribes or complaints.
  * See [Messages → lifecycle](/concepts/messages#the-message-lifecycle) for
    the updated status reference.

  <Note>
    Filtering by `status: "opted_out"` was the de facto way to find a
    recipient's unsubscribe/complaint history; this fix narrows it to
    pre-send skips only, with no replacement query yet. Subscribe to
    `message.opted_out` / `message.complained` to track consent events going
    forward.
  </Note>
</Update>

<Update label="July 26, 2026" description="New suppressed message status (managed sending)" tags={["Dashboard"]}>
  ## `suppressed` message status

  Managed sending (AWS SES) now runs **SES email validation** against every
  recipient before attempting delivery. When AWS rejects a recipient as
  invalid, or as already on the account's suppression list, the message lands
  in a new terminal `suppressed` status instead of `failed` — keeping bounce
  metrics and your logs accurate about what was actually attempted versus what
  never left the building. A per-workspace suppression-rate signal is shown on
  the channel detail page.

  `suppressed` only applies to managed sending; bring-your-own-provider
  connections are unaffected. The `Message.status` type in the TypeScript,
  PHP, and Python SDKs will include `"suppressed"` in an upcoming release —
  until then, treat it as a value the API and dashboard can already return.

  <Note>
    Update: as of `@senderkit/sdk@0.13.0` (see the [July 29, 2026 entry](#july-29-2026)
    above), `suppressed` is included in the TypeScript SDK's exported
    `Message.status` values. The Python and PHP SDKs pick it up in their own
    upcoming releases.
  </Note>

  <Note>
    Update: a matching **`message.suppressed`** [webhook event](/webhooks#events)
    fires whenever a send lands in this status, so you can react to it without
    polling. See the [Webhooks → Events](/webhooks#events) reference.
  </Note>
</Update>

<Update label="July 26, 2026" description="Inbound email: custom domains and catch-all addresses" tags={["Dashboard"]}>
  ## Custom inbound domains and catch-all addresses

  Two additions to inbound email:

  * **Custom inbound domains** — receive mail at addresses on your own domain
    (e.g. `inbound.acme.com`) instead of only the shared
    `{slug}.in.senderkit.email`. Claim a domain from the dashboard or
    `POST /v1/inbound/domains`, publish the DNS records it returns (an MX,
    plus a DKIM TXT proving ownership — skipped if the domain already has a
    verified sending domain, which reuses its identity), and a background
    sweep verifies it. New `senderkit_inbound_domains_list` / `_create` /
    `_delete` MCP tools mirror the REST surface.
  * **Catch-all addresses** — create an address with `localPart: "*"` to
    receive mail sent to any local part on a domain that no exact address
    already claims. An exact-match address always takes priority over the
    catch-all.

  Both count against the same plan-gated inbound address cap as any other
  address.

  <Note>
    Update: full reference detail for both — DNS records, verification
    states, and API/MCP parameters — is now live on
    [Inbound Email → Custom domains](/concepts/inbound#custom-domains) and in
    the [MCP tools reference](/mcp/tools#inbound).
  </Note>
</Update>

<Update label="July 24, 2026" description="Inbound email: receive mail at addresses on your workspace's domain" tags={["Dashboard", "Webhooks"]}>
  ## Receive email, not just send it

  Workspaces can now provision **inbound addresses** on a shared receiving
  domain (`{slug}.in.senderkit.email`) and get mail sent to them parsed and
  delivered as a new **`message.received`** [webhook event](/webhooks#events) —
  with an optional forward to a real inbox.

  * **New Inbound dashboard page** — create and remove addresses, optionally
    binding one to a specific webhook endpoint or a forwarding address.
  * **New `/v1/inbound/*` API routes** — list/create/delete addresses, list and
    read received messages (parsed body, headers, SES spam/virus/SPF/DKIM
    verdicts), and fetch attachments or the original raw MIME source (30-day
    retention). Requires an API key with the new **`inbound`**
    [scope](/authentication#scopes).
  * **Plan-gated address caps** — Free 1, Starter 10, Pro 50. A received live
    message counts against the same monthly message quota as outbound sends.

  See [Inbound Email](/concepts/inbound) for the full reference.
</Update>

<Update label="July 22, 2026" description="Templates: per-template email theme (Design tab)" tags={["Dashboard"]}>
  ## Style emails without touching HTML

  Email templates gained a new **Design** tab in the editor for setting a per-template
  visual theme:

  * **Fonts** — independent heading and body fonts, picked from a curated set of
    web-safe and Google fonts, or a custom Google Font by name.
  * **Colors** — page background, content background, and body, heading, and link
    text colors.
  * **Content width** — the email's maximum content width, 320–900px.

  The theme applies consistently in the editor canvas, the Preview tab, and sent
  messages. It's fully optional — templates with no theme set render exactly as
  before. Mail clients without web-font support (Gmail, Outlook) fall back to a
  close web-safe equivalent for custom fonts.

  See [Templates → Styling with the Design tab](/concepts/templates#styling-with-the-design-tab-email-only).
</Update>

<Update label="July 22, 2026" description="Templates: images stay responsive on mobile" tags={["Dashboard"]}>
  ## Resized images no longer squeeze on narrow screens

  Images resized in the email editor previously kept a fixed height on mobile, so
  shrinking the width to fit a narrow screen distorted the aspect ratio. Resized
  images now scale proportionally on any screen size.

  A new size toggle appears when an image is selected, letting you choose whether
  width or height stays fixed after a resize — useful for a fixed-height banner or
  badge that shouldn't scale like a normal inline image.
</Update>

<Update label="July 22, 2026" description="BYOP SES: bounce and complaint events fixed for the standard AWS setup" tags={["Integration"]}>
  ## Bounce/complaint/delivery events fixed for identity-level SES notifications

  Bring-your-own-provider SES connections that follow AWS's default bounce/complaint
  setup — SNS notifications wired directly onto the verified identity, without a
  configuration set — previously had their bounce, complaint, and delivery events
  silently dropped. Affected messages stayed stuck at `sent`, and complaint
  suppression / the `message.opted_out` [webhook](/webhooks) never fired. Both
  notification shapes SES can send are now recognized; no changes needed on your
  end.
</Update>

<Update label="July 19, 2026" description="Email: preheader text now shows in the inbox preview" tags={["Integration"]}>
  ## Preheader now renders as the actual inbox preview snippet

  The **preheader** field (set in the template editor, and previously visible only
  in the editor's own inbox preview mock) now reaches recipients' inboxes. Mail
  clients build the preview snippet shown next to the subject line from the
  message itself, and that wasn't happening consistently — so the snippet
  recipients saw was often scraped from the body instead of the preheader you
  set. This is fixed across every provider connection; no changes needed on
  your end.
</Update>

<Update label="July 17, 2026" description="Email: cc, bcc, replyTo, and attachments now honored on all providers" tags={["Integration"]}>
  ## `cc`, `bcc`, `replyTo`, and `attachments` now work on every connection type

  These [send-time envelope fields](/sdks/typescript#send) previously had
  no effect on some provider connections: the send would succeed, but cc/bcc
  recipients wouldn't receive the message, replies would go to the From address
  instead of `replyTo`, and attachments would be silently dropped. All four
  fields now behave identically across every supported provider, including
  managed sending. No API or field changes — this is a behavior fix only.
</Update>

<Update label="July 16, 2026" description="Email: From display name quoting fix" tags={["Integration"]}>
  ## Sender display names are now RFC-correct on every provider

  Display names containing commas or other special characters (for example,
  `Acme, Inc.`) are now correctly quoted in the `From` header on every provider
  connection — previously an unquoted name with a comma could produce a
  malformed header. Display names (`fromName`) are also now consistently
  applied on every provider connection.
</Update>

<Update label="July 12, 2026" description="Provider webhooks: reactivation no longer marked as opted_out" tags={["Integration"]}>
  ## Recipient reactivations are no longer treated as an opt-out

  A resubscribe/reactivation event from a provider could previously be
  misclassified the same way as an unsubscribe, incorrectly flipping the
  message to `opted_out` status and firing a `message.opted_out`
  [webhook](/webhooks). Reactivation events are no longer treated as opt-outs;
  only genuine suppressions do.
</Update>

<Update label="July 11, 2026" description="Provider webhooks: delivery status reliability fix" tags={["Integration"]}>
  ## More reliable status updates on inbound provider events

  Some provider delivery, bounce, and engagement webhook events could fail to
  match their originating message, leaving affected messages from that
  provider connection stuck at `sent` instead of progressing to `delivered` /
  `failed`, with opens and clicks not recorded. This correlation issue is
  fixed; no action is needed on your end.
</Update>

<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.

  <Note>
    Update: `@senderkit/sdk@0.13.0` / `@senderkit/cli@0.8.0` has shipped — see
    the [July 29, 2026 entry](#july-29-2026) above.
  </Note>
</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 19 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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.