Email

Send an email campaign

Create an email campaign for an audience, schedule a run, and inspect or control delivery progress.

Use a campaign for a named broadcast whose recipients come from contact IDs or tags. Creating a campaign sends nothing. A run snapshots the definition and starts immediately or at a future time.

Define the campaign

Send campaigns as email. A run supports at most 25,000 resolved recipients. Choose exactly one content source:

  • Inline email requires subject and at least one of html or text.
  • Template email references exactly one published template by templateSlug or templateId, with optional templateData. Do not mix it with inline subject, html, or text.

The audience must use includeTags or contactIds as its source. excludeTags removes matches. Contacts resolve when the run starts. After the first run is created, the campaign definition is frozen and updates return a conflict.

Use the Promise SDK

send-campaign.ts
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const campaign = await samva.campaigns.create({
  name: "August newsletter",
  channel: "email",
  content: {
    channel: "email",
    email: { subject: "What's new", html: "<h1>August updates</h1>" },
  },
  audience: { includeTags: ["newsletter"], excludeTags: ["bounced"] },
});

const run = await samva.campaigns.scheduleRun({
  id: campaign.id,
  scheduledFor: "2026-08-01T09:00:00Z",
  idempotencyKey: "august-newsletter-v1",
});

console.log("Campaign run:", run.id);

Use the Effect SDK

send-campaign-effect.ts
import { Effect } from "effect";
import * as Campaigns from "samva/effect/campaigns";
import * as Client from "samva/effect/client";

const program = Effect.gen(function* () {
  const campaign = yield* Campaigns.create({
    name: "August newsletter",
    channel: "email",
    content: {
      channel: "email",
      email: { subject: "What's new", html: "<h1>August updates</h1>" },
    },
    audience: { includeTags: ["newsletter"] },
  });

  return yield* Campaigns.scheduleRun({
    id: campaign.id,
    scheduledFor: new Date("2026-08-01T09:00:00Z"),
    idempotencyKey: "august-newsletter-v1",
  });
}).pipe(Effect.provide(Client.layerFetch({ apiKey: process.env.SAMVA_API_KEY! })));

await Effect.runPromise(program);

Use the REST API

curl -X POST https://api.samva.dev/v1/campaigns \
  -H "X-API-Key: $SAMVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "August newsletter",
    "channel": "email",
    "content": {
      "channel": "email",
      "email": { "subject": "What is new", "html": "<h1>August updates</h1>" }
    },
    "audience": { "includeTags": ["newsletter"] }
  }'

curl -X POST https://api.samva.dev/v1/campaigns/<campaign-id>/runs \
  -H "X-API-Key: $SAMVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduledFor": "2026-08-01T09:00:00Z",
    "idempotencyKey": "august-newsletter-v1"
  }'

Omit scheduledFor to start immediately. When present, it must be an absolute ISO-8601 instant with Z or a numeric offset. Campaign runs do not accept timezone metadata. See the generated campaigns reference for every operation.

Use the CLI

samva campaigns create --name "August newsletter" \
  --subject "What's new" --html "<h1>August updates</h1>" \
  --include-tags newsletter
samva campaigns runs create <campaign-id> --at 2026-08-01T09:00:00Z \
  --idempotency-key august-newsletter-v1
samva campaigns runs get <campaign-id> <run-id>
samva campaigns runs recipients <campaign-id> <run-id> --status failed

Use samva campaigns create --dry-run to validate the definition without creating it. Use samva campaigns runs pause, resume, or cancel with the campaign ID and run ID to control a run.

Use MCP

Call campaigns_create, then campaigns_schedule_run. Track progress with campaigns_get_run and inspect outcomes with campaigns_list_recipients. The campaigns_control_run tool accepts pause, resume, or cancel.

Idempotency, controls, errors, and usage

Pass an idempotencyKey when scheduling a run. It is scoped to the organization, and replaying the same key returns the original run. Pausing stops new dispatch and resuming safely re-enqueues unsettled recipients. Cancelling a future scheduled run is terminal. Cancellation after dispatch begins is best effort, so already claimed or provider-submitted deliveries may finish.

If email review is no longer clear immediately before a recipient reaches the provider, the run is paused with an onboarding_review:<reason> hold. No provider call is made for that recipient. The run stays paused after approval until you explicitly resume it with the dashboard, CLI, API, or campaigns_control_run. Unsettled recipients are then re-enqueued through the normal fresh review fence; Samva never performs an automatic replay.

Each recipient follows the normal email send and usage path when dispatched. Run totals distinguish dispatched, failed, and skipped recipients; recipient records carry the outcome. Invalid state transitions and edits after the first run return conflicts. See the error reference for shared API error handling.

Related

On this page