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
Campaigns are email-only and support at most 25,000 resolved recipients per run. Choose exactly one content source:
- Inline email requires
subjectandhtml;textis an optional fallback. - 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"] },
});
if (campaign.error) throw new Error("Could not create campaign");
const run = await samva.campaigns.scheduleRun({
id: campaign.data.id,
scheduledFor: "2026-08-01T09:00:00Z",
idempotencyKey: "august-newsletter-v1",
});
if (run.error) throw new Error("Could not schedule campaign");Use the Effect SDK
import { Effect } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { createClient } from "samva/effect";
const program = Effect.gen(function* () {
const samva = yield* createClient({ apiKey: process.env.SAMVA_API_KEY! });
const campaign = yield* samva.campaigns.create({
payload: {
name: "August newsletter",
channel: "email",
content: {
channel: "email",
email: { subject: "What's new", html: "<h1>August updates</h1>" },
},
audience: { includeTags: ["newsletter"] },
},
});
return yield* samva.campaigns.scheduleRun(campaign.id, {
payload: {
scheduledFor: "2026-08-01T09:00:00Z",
idempotencyKey: "august-newsletter-v1",
},
});
}).pipe(Effect.provide(FetchHttpClient.layer));
await Effect.runPromise(program);Use the REST API
curl -X POST https://api.samva.app/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.app/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 schedule <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 samva_campaigns_create, then samva_campaigns_schedule_run. Track progress with
samva_campaigns_get_run and inspect outcomes with samva_campaigns_list_recipients. The
samva_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.
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.