Send a broadcast campaign

Your first newsletter touches three pieces: a broadcast stream that carries the marketing rules, a list of opted-in subscribers, and a campaign that fans out into one message per person. This recipe wires them together in order. Each piece has its own reference; here you just chain them.

Broadcast mail answers to stricter rules than a receipt or a password reset, and CamelMailer enforces four of them once a stream is typed broadcast: one-click unsubscribe, an opt-in gate, a compliance footer, and stream-scoped suppressions. Set the stream up once and the rest of the campaign inherits all of it.

1. Create a broadcast stream and set the footer address

A broadcast stream is a message stream whose stream_type is broadcast. Create it in the dashboard under Server, then Streams, or over the API:

terminal
curl -s -X POST "$API/api/v2/server/streams" \
  -H "X-Server-API-Key: $SERVER_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Newsletter", "stream_type": "broadcast"}'

Before you send anything real, set the server's broadcast postal address under Server, then Settings, then Broadcast postal address. CAN-SPAM requires a physical address in every marketing message, and the footer ships on each broadcast whether or not you set one. When it is unset the address line is missing and the dashboard raises a compliance warning on the stream. Give the stream its own IP pool here too, so its reputation stays separate from your transactional mail. The four guarantees and the postal-address setting are covered in Broadcast streams.

2. Add opt-in subscribers

A broadcast reaches only addresses that have opted in. Consent is a per-stream record, so subscribing someone to newsletter says nothing about any other stream. Add people one at a time or in bulk:

  • By hand.Use the add box on the stream's Subscribers tab, or POST one address to the subscribers endpoint (status defaults to subscribed).
  • From a CSV. The Subscribers tab takes a CSV of addresses with a template you download first, so your columns match the importer. The shared import flow, including that template step, is in Import & export.
terminal
# Opt one recipient in (status defaults to subscribed)
curl -s -X POST "$API/api/v2/server/streams/newsletter/subscribers" \
  -H "X-Server-API-Key: $SERVER_KEY" -H "Content-Type: application/json" \
  -d '{"address": "reader@example.com"}'
A broadcast send checks every recipient at enqueue time and rejects the whole request with 422, naming the first address that has not opted in. Build the subscriber list before the campaign, because a send to a non-subscriber never queues.

3. Compose the campaign and send

A campaign is one broadcast as a first-class record. Write the subject, From address, and HTML and text bodies once in the same drag-and-drop block editor the template editor uses, aim it at the broadcast stream, then pick Send now, Schedule, or Save as draft. When it sends, CamelMailer walks the stream's currently subscribed addresses and expands the campaign into one message per person, each tagged with the campaign's campaign_id. That tag is what lets the stats roll up over exactly this campaign's mail. The lifecycle, scheduling, and the API calls are in Campaigns.

terminal
# Create and send at once
curl -s -X POST "$API/api/v2/server/campaigns" \
  -H "X-Server-API-Key: $SERVER_KEY" -H "Content-Type: application/json" \
  -d '{
    "stream": "newsletter",
    "from": "news@acme.example",
    "subject": "This week at Acme",
    "html_body": "<h1>Hello</h1><p>Here is what is new.</p>",
    "text_body": "Hello. Here is what is new.",
    "send_now": true
  }'

Each expanded message rides the same broadcast send path as a one-off, so it carries the per-recipient one-click List-Unsubscribe header and the compliance footer already. A recipient who unsubscribed before the send is skipped.

4. Let opt-outs stay inside marketing

Every broadcast message carries an RFC 8058 one-click unsubscribe, so Gmail, Apple Mail, and Yahoo render a native Unsubscribe button. When someone uses it, CamelMailer writes a suppression scoped to that one stream and flips their subscription to unsubscribed. The scope is the point: a newsletter opt-out blocks the newsletter stream and leaves the same person's password resets and receipts flowing on the transactional stream. Spam complaints arriving through a feedback loop close consent the same way, automatically. How scope is stored and why it keeps transactional mail safe is in Suppressions.

5. Read the results

Fetch the campaign back and it carries a stats object aggregated over the messages it produced: delivered, failed, opened, clicked, and unsubscribed. In the dashboard the campaign detail view shows counter tiles and an engagement donut, with Recipients and Messages tabs that filter by campaign_id for per-recipient drill-down.

terminal
curl -s "$API/api/v2/server/campaigns/42" -H "X-Server-API-Key: $SERVER_KEY"

Opens and clicks come from CamelMailer's tracking, so they populate where tracking is enabled. The full stat table and how each figure is counted are in Campaigns.

Without a running delivery worker, a campaign still reaches sent because its counter reflects messages enqueued, and the queue holds until a worker drains it. For an end-to-end local test, run the worker alongside the web server. See Campaigns for the local-dev note.