Upgrade samva to 0.30.0 and @samva/cli to 0.28.0. Change a call that passes an idempotency key in a JSON body or query string, imports @samva/mailbox, or puts idempotencyKey inside samva templates create --data or samva events send --data. Search for those, then let the type checker find the rest. Each package's full notes are on its GitHub release.
What you need to do
- Idempotency keys. Pass
{ idempotencyKey }as the second argument on the Promise and Effect clients. Raw HTTP sends theIdempotency-Keyheader. Remove the key from JSON bodies and query strings. A key contains 1 to 255 characters. See Idempotency.
email.send and messages.send already take that second argument. Scheduled messages, campaigns, templates, mailbox sends, and the other idempotent calls move to the same shape.
Both clients send one fresh key on each idempotent call and reuse it when they retry that call. { idempotencyKey } overrides a client idempotency hook, and the hook overrides the generated key. On the Promise client, an Idempotency-Key header passed in call options is ignored.
contacts.startImport retries only when mode is "new". A resume is not retried. The header is required for a new import and optional for a resume. After an ambiguous resume response, read contacts.getImportJob.
// Before
await samva.scheduledMessages.create({
send: {
to: [{ email: "ada@example.com" }],
channel: "email",
email: { subject: "Reminder", html: "<p>See you tomorrow.</p>" },
},
scheduledFor: "2026-08-01T09:00:00Z",
timezone: "America/New_York",
idempotencyKey: "reminder-ada-2026-08-01",
});
await samva.mailboxes.sendMessage({
id: "mbx_...",
to: ["ada@example.com"],
subject: "Hello",
text: "Hello from the mailbox",
"idempotency-key": "support-message-123",
});
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY!, maxRetries: 3 });
await samva.scheduledMessages.create(
{
send: {
to: [{ email: "ada@example.com" }],
channel: "email",
email: { subject: "Reminder", html: "<p>See you tomorrow.</p>" },
},
scheduledFor: "2026-08-01T09:00:00Z",
timezone: "America/New_York",
},
{ idempotencyKey: "reminder-ada-2026-08-01" },
);
import { Effect } from "effect";
import * as Client from "samva/effect/client";
import * as Templates from "samva/effect/templates";
const program = Templates.create(
{ name: "Receipt" },
{ idempotencyKey: "receipt-template" },
).pipe(Effect.provide(Client.layerFetch({ apiKey: process.env.SAMVA_API_KEY! })));
await Effect.runPromise(program);
curl -X POST https://api.samva.dev/v1/messages/scheduled \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reminder-ada-2026-08-01" \
-d '{
"send": {
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"email": { "subject": "Reminder", "html": "<p>See you tomorrow.</p>" }
},
"scheduledFor": "2026-08-01T09:00:00Z",
"timezone": "America/New_York"
}'
- CLI. Pass
--idempotency-key. RemoveidempotencyKeyfrom--data.
samva templates create --data '{"name":"Welcome","projectName":"welcome-email"}' --idempotency-key welcome-email-v1
samva events send --data '{"contact":{"email":"ada@example.com"},"name":"trial.started"}' --idempotency-key trial-42
- Mailbox extensions. Import
defineExtensionfromsamva/mailbox, test helpers fromsamva/mailbox/test, andcreateWebhookHandlerfromsamva/mailbox/webhook.@samva/mailboxis removed. A handler receives the normal client and callssamva.mailboxes.reply({ id, messageId, text }).samva extensions initscaffolds these imports. See Build mailbox extensions.
// Before
import { defineExtension } from "@samva/mailbox";
await samva.messages.reply(event.data.mailboxId, event.data.message.id, {
text: "Thanks, we got it.",
});
import { defineExtension } from "samva/mailbox";
export default defineExtension({
manifest: {
name: "acknowledge",
displayName: "Acknowledge new mail",
version: "1.0.0",
runtime: "hosted",
events: ["mailbox.message.received"],
permissions: ["read", "send"],
},
on: {
"mailbox.message.received": async (event, { samva }) => {
await samva.mailboxes.reply({
id: event.data.mailboxId,
messageId: event.data.message.id,
text: "Thanks, we got it.",
});
},
},
});
Also new
- Retries. The Promise client retries reads and idempotent mutations. Set
maxRetriesoncreateClientor on one call.0turns retries off. The default is 3 retries after the first attempt. The Effect client retries the same calls. Turn that off withRetry.layerDisabledfromsamva/effect/retry. - List iterators. A Promise cursor list exposes lazy async iterators.
pagesyields page envelopes anditemsyields each item. Effect lists keepStreamcompanions on.pages()and.items().
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
for await (const contact of samva.contacts.list.items({ limit: 100 })) {
console.log(contact.id);
}
- OAuth organization. Pass
orgSlugwithauthTokento select an organization. An API key already selects its organization. - Effect peer.
samvaand@samva/webhooksaccept optional peereffect@^4.0.4on the Effect entrypoints. The Promise client does not needeffect. - Template commands.
@samva/cli0.28 runssamva templateswith@samva/markupand@samva/vite0.22.0. Directrendercalls pass{ target: "email" }(0.29), and compiled templates carrysml: 2. An issue layout can print the title and date withIssueTitleandIssueDate. Templates that already build keep the same output on 0.22.0.
Preview · Available to selected organizations.
Newsletters. topicIssues, topicPublicationDomains, topic issue routes, and topic archive fields are removed. Use newsletters, newsletters.issues, and newsletters.domains on the Promise client, or samva/effect/newsletters, samva/effect/newsletter-issues, and samva/effect/newsletter-domains. The CLI is samva newsletters. Consent, subscriptions, and campaign audiences still use topicId. See Newsletters.
Preview · Available to selected organizations.
Agents. Replace integrationIds on an agent definition with integrations. Each grant is { integrationId, tools: { kind: "all" }, requireApproval: [] }. tools.kind can be readOnly, or selected with names. Publishing rejects a grant that names an integration the organization has not installed, or a tool its app does not have.