This page tracks releases of SenderKit’s developer libraries — the
TypeScript SDK, the PHP SDK, the
CLI, and the MCP server.
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.
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 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.
Per-message From overrides, now identical on templated and raw sends
@senderkit/sdk@0.12.0 / @senderkit/cli@0.7.0Email 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.
blockedReason removed from the SDK type
@senderkit/sdk@0.11.0 / @senderkit/cli@0.6.4The June 19 entry 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.
Inbound webhook retries are now deduplicated
Sources that deliver to a SenderKit inbound hook endpoint — including the
Supabase auth email hook — 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.
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.
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 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.
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.
blocked message status
@senderkit/sdk@0.10.0 / @senderkit/cli@0.6.3SenderKit 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 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 for the full
status reference.
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.
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
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.
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 for template
variable reference, limits, and troubleshooting.
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).
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.
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.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 for the full API, Laravel notification
channel, mail transport, and Symfony bundle setup.
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: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.
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: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 and the
SDK error reference.
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.# 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.
Lean template and message reads (TemplateVersion.content removed)
@senderkit/sdk@0.7.0 / @senderkit/cli@0.5.1templates.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.
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 for
parameters.
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.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.
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: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: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.
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.
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.
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.
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 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 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 and tools reference.