Skip to content

Campaigns and stage plans

A campaign sends one template version to one segment. It goes out in stages, most engaged contacts first, and checks bounce and complaint rates before each stage after the first. Each email is sent at most once, even if a step runs twice.

PlanStages (share of the audience)Wait between stagesUse it for
warmup2%, 8%, 20%, 30%, 40%24 hoursYour first send from a new account or domain, or after 90 or more days without sending.
standard10%, 90%2 hoursEvery send after that.
single100%—Lists under 5,000 contacts.

Choose a plan with "stagePlan": { "type": "preset", "name": "standard" }. Without a stagePlan, a campaign uses warmup for your first send and after 90 days without sending, single for lists under 5,000, and standard otherwise. You can also give your own plan: { "type": "custom", "stages": [25, 75], "waitMinutes": 180 }, with up to 10 stages that add up to 100 and a wait of up to a week.

Stages are numbered from 0 in the API and CLI: a campaign’s stageIndex, each send record’s stage, the recipients stage filter and the report’s byStage. The console numbers them from 1, so its stage 1 is the API’s stage 0.

You need a template version. Create one if you haven’t:

Terminal window
curl -X POST "$BASE/templates" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
"name": "Product news",
"version": {
"subject": "What is new at Example Co",
"preheader": "Three things we shipped this month.",
"html": "<p>Hi {{ contact.fields.FNAME | default: \"there\" }},</p><p>Three things we shipped.</p><p><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a><br>{{ footer_address }}</p>",
"fromName": "Example Co", "fromEmail": "news@example.com"
}
}'

Then create the campaign, with a saved segment (segmentId) or a definition (segment):

Terminal window
curl -X POST "$BASE/campaigns" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"name":"Product news, October","templateId":"<tp_...>","templateVersion":1,"segment":{"all":[{"type":"tag","tag":"customer"}]},"stagePlan":{"type":"preset","name":"standard"}}'

The campaign starts as a draft. Until you schedule it, you can change anything, sending the version you read in If-Match:

Terminal window
curl -X PATCH "$BASE/campaigns/<cm_...>" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-H 'If-Match: 1' -d '{"name":"Product news, October 2026"}'

Check it worked: the campaign has an id starting cm_ and "state": "draft".

A draft you don’t want can be deleted: DELETE /campaigns/<cm_...> answers 204. Once a campaign is scheduled it can’t be deleted, only cancelled, so its history stays.

Test sends go to up to 10 addresses, with [TEST] before the subject. They don’t count in the campaign’s stats, and they come out of your quota reserve.

Terminal window
curl -X POST "$BASE/campaigns/<cm_...>/test" -H "Authorization: Bearer $KEY" -H 'Idempotency-Key: product-news-test-1' \
-H 'Content-Type: application/json' -d '{"to":["you@example.com","colleague@example.com"]}'

Add "contactId" to render the test with one of your contacts’ fields.

Check it worked: the tests arrive, the merge fields look right, and the unsubscribe link opens a confirmation page.

Terminal window
curl -X POST "$BASE/campaigns/<cm_...>/schedule" -H "Authorization: Bearer $KEY" -H 'Idempotency-Key: product-news-schedule'

It starts now, or at the campaign’s sendAt if that’s later. Scheduling runs the pre-send checks first, and refuses with 422 schedule_checks_failed if any fails, listing each with its fix:

  • the template version renders, with the unsubscribe link and footer,
  • your footer address is set in settings,
  • your account is connected and your subscription is active,
  • SES production access and sending are on, and DKIM, MAIL FROM and DMARC pass,
  • the segment has at least one sendable contact.

Check it worked: the campaign is scheduled, then sending once the first stage starts.

Terminal window
curl "$BASE/campaigns/<cm_...>" -H "Authorization: Bearer $KEY"
curl "$BASE/campaigns?state=sending" -H "Authorization: Bearer $KEY"
  1. Audience. When the campaign starts, the matching contacts are sorted by engagement score, highest first, and split into the plan’s stages and batches of 50. Anyone who unsubscribes later is skipped when their batch is sent.
  2. Gate. Before each stage after the first, the campaign checks its hard bounces (under 2% of sent) and complaints (under 0.1% of delivered), once 1,000 emails are delivered. Suppression-list and undetermined bounces don’t count. If either is over, the campaign pauses.
  3. Quota. Each stage sends what fits in your SES daily quota, after your other campaigns and the quota reserve. If there’s more, it carries on an hour later.
  4. Sending. Each batch is sent through your SES with the rhino-relay-campaigns configuration set, which all your campaigns share. Every email carries tags naming its campaign, so events are matched to the right one. Before each batch the sender checks the campaign is still sending, so pausing one campaign stops it within a few emails and never affects another.
  5. Finish. When the last stage is sent, the campaign waits for stragglers and moves to completed. Emails whose outcome SES never confirmed are counted as unknown.

Each finished stage publishes campaign.stage_completed, and the end publishes campaign.completed.

State Means What you can do
draft Not scheduled yet Change it, test it, schedule it, delete it
scheduled Waiting for its start time Cancel it
sending Stages are going out Pause or cancel it
paused Stopped by you, a rate check, or SES Resume or cancel it
completed Everything was sent Read its report
cancelled Stopped for good. Recipients who were never sent to are marked skipped. Read its report

See Reports for what was sent, delivered, opened and clicked.