# Upgrading to samva 0.30 and the CLI 0.28
URL: https://samva.dev/changelog/upgrade-to-samva-0-30
> samva 0.30 passes idempotency keys as options, retries safe calls, and moves mailbox extensions onto samva/mailbox. Upgrade the CLI to 0.28.
- Published: 2026-10-29
- Tags: sdk, cli, api

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](https://github.com/SamvaHQ/samva-typescript/releases).

**What you need to do**

1. **Idempotency keys.** Pass `{ idempotencyKey }` as the second argument on the Promise and Effect clients. Raw HTTP sends the `Idempotency-Key` header. Remove the key from JSON bodies and query strings. A key contains 1 to 255 characters. See [Idempotency](/docs/developers/rest-api#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`.

```ts sdk-typecheck-skip="Historical release excerpt; request bodies and mailbox calls no longer accept an idempotency key"
// 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",
});
```

```ts title="schedule-with-key.ts"
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" },
);
```

```ts title="create-template-effect.ts"
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);
```

```bash
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"
  }'
```

2. **CLI.** Pass `--idempotency-key`. Remove `idempotencyKey` from `--data`.

```bash
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
```

3. **Mailbox extensions.** Import `defineExtension` from `samva/mailbox`, test helpers from `samva/mailbox/test`, and `createWebhookHandler` from `samva/mailbox/webhook`. `@samva/mailbox` is removed. A handler receives the normal client and calls `samva.mailboxes.reply({ id, messageId, text })`. `samva extensions init` scaffolds these imports. See [Build mailbox extensions](/docs/mailboxes/extensions).

```ts sdk-typecheck-skip="Historical release excerpt; mailbox extensions no longer import @samva/mailbox or call a positional reply"
// Before
import { defineExtension } from "@samva/mailbox";

await samva.messages.reply(event.data.mailboxId, event.data.message.id, {
  text: "Thanks, we got it.",
});
```

```ts title="src/extension.ts"
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 `maxRetries` on `createClient` or on one call. `0` turns retries off. The default is 3 retries after the first attempt. The Effect client retries the same calls. Turn that off with `Retry.layerDisabled` from `samva/effect/retry`.
- **List iterators.** A Promise cursor list exposes lazy async iterators. `pages` yields page envelopes and `items` yields each item. Effect lists keep `Stream` companions on `.pages()` and `.items()`.

```ts title="list-contacts.ts"
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 `orgSlug` with `authToken` to select an organization. An API key already selects its organization.
- **Effect peer.** `samva` and `@samva/webhooks` accept optional peer `effect@^4.0.4` on the Effect entrypoints. The Promise client does not need `effect`.
- **Template commands.** `@samva/cli` 0.28 runs `samva templates` with `@samva/markup` and `@samva/vite` 0.22.0. Direct `render` calls pass `{ target: "email" }` (0.29), and compiled templates carry `sml: 2`. An issue layout can print the title and date with `IssueTitle` and `IssueDate`. Templates that already build keep the same output on 0.22.0.

<Staged flag="newsletters">

**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](/docs/channels/email/newsletters).

</Staged>

<Staged flag="agents">

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

</Staged>

## Links

- [TypeScript SDK](/docs/developers/typescript-sdk)
- [Idempotency](/docs/developers/rest-api#idempotency)
- [Mailbox extensions](/docs/mailboxes/extensions)
- [samva 0.30.0 release notes](https://github.com/SamvaHQ/samva-typescript/releases/tag/samva%400.30.0)
- [@samva/cli 0.28.0 downloads](https://github.com/SamvaHQ/samva-cli/releases/tag/v0.28.0)

## Related

- [Upgrading to samva 0.28 and the CLI 0.26](/changelog/upgrade-to-samva-0-28)
- [All changelog entries](/changelog)
