Skip to content

Your first campaign — audience, import, compose, send

Send your first broadcast — create an audience, import or paste in your list, compose the email, test it, then approve and fan out. Both the guided in-app flow and the CLI path.

A campaign is one-to-many email: authored, reviewed, then fanned out to an audience. This is the highest-blast-radius thing you can do, so it's built around two locks — content review and a send capability — and an idempotent ledger that cannot double-send. This guide takes you from an empty Lists section to a sent broadcast.

Before you start, you need a verified sending domain (connect your domain first). To confirm you're ready to send a broadcast — and see exactly what's missing if not — ask:

tinbox get-send-readiness --lane broadcast --json

It returns an ordered checklist with fix hints and deep links for the broadcast lane. The in-app campaign flow shows the same readiness inline.

The guided flow (in the app)

From an empty Lists section, New campaign opens a short guided flow that sequences the prerequisites instead of assuming them:

  1. Pick the kind — broadcast (one email to a whole list), personalized (the same campaign, rewritten per recipient), or cold outreach (1:1-at-bulk to people who haven't opted in — that's a different setup). For a first send, pick broadcast.
  2. Readiness — the flow shows the broadcast checklist inline; unmet items link to their fix. You can proceed on soft warnings; hard blockers (no verified domain) show the fix in-flow.
  3. Audience — pick an existing list or create one inline. The import hub is embedded here, so pasting or uploading a list is one step, not a settings detour.
  4. Compose — subject and body, with template suggestions for first-timers.
  5. Review and schedule — send now or schedule; the same review/approve mechanics as the direct path. The end state deep-links to the campaign's detail view.

The wizard is the first-run ramp — it's skippable at every step, and the direct create path is always there for repeat sends. Wizard state lives in the URL, so a refresh mid-flow loses nothing.

The same path, on the CLI

Every step has a CLI equivalent — useful for scripting, or for an agent doing the work.

Create an audience

tinbox create-audience --name "Newsletter" \
  --fromName "Acme" --fromEmail news@acme.dev --replyTo hello@acme.dev

Add --doubleOptin to require subscribers to confirm by email before they count as subscribed.

Bring in your list

Paste or upload a CSV in the import hub, add people one at a time, or import from an existing tool.

Add a single subscriber:

tinbox add-subscriber --audienceId aud_… \
  --email ada@example.com --name "Ada Lovelace" --tags vip

Import a whole Mailchimp audience (members, merge fields, and tags) — the API key and every fetch run locally on your machine, never on the worker:

tinbox import mailchimp --api-key <key-usXX>

Imports are idempotent: re-running updates or skips, never duplicates. See Import existing mail and lists for Gmail, IMAP, mbox, and campaign imports.

List your audiences and their member counts any time:

tinbox audiences list --json
tinbox audiences members aud_… --json

Compose the campaign

tinbox create-campaign --audienceId aud_… \
  --title "March update" \
  --subject "What's new at Acme" \
  --content "# Hello\n\nHere's what shipped this month…"

The campaign starts in draft. You can inspect its status, ledger progress, and analytics with:

tinbox campaigns status cmp_…

Test it first

Send a single safe preview to yourself before the real fan-out. It never touches the recipient ledger:

tinbox campaigns test cmp_… --to you@acme.dev

Approve, then send

Fan-out needs both locks. Approval is the content lock — the state machine (draft → in_review → approved) refuses to send from draft or in_review:

tinbox approve-campaign --id cmp_…

Dispatch additionally needs the authority lock — the calling token must hold the newsletter:send capability on a write scope. Without it the API returns FORBIDDEN and the CLI exits non-zero with a clear authz error; the CLI gets no special powers. With it:

tinbox campaigns send cmp_…

The fan-out is ledgered and idempotent — one row per (campaign, recipient), enqueued to Cloudflare Queues, checked before each send. A retry or double-run only enqueues un-sent rows, so it cannot double-send the list. Every campaign honors one-click List-Unsubscribe immediately.

After it sends

Check how it did — opens, clicks, unsubscribes, and the recipient-ledger breakdown:

tinbox get-campaign-report --id cmp_…

Treat opens as a soft signal: Apple Mail Privacy Protection pre-fetches the pixel and inflates the number.

Next steps