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
subjectand at least one ofhtmlortext. - Template email references exactly one published template by
templateSlugortemplateId, with optionaltemplateData. Do not mix it with inlinesubject,html, ortext.
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
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
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 failedUse 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.