Skip to content
MiniMailer
Docs
Toggle sidebar
Quick links
Documentation
Knowledge Base
API Reference
Changelog
No results found

Campaigns

A campaign sends one message to an audience — a named list of contacts. A transactional send targets a single recipient triggered by your code; a campaign targets a list. Both send from a verified domain and feed the same event tracking and webhooks.

Create an audience and add contacts

An audience is a named list of contacts. Create one, then add contacts individually or in bulk:

  • POST /audiences — create the audience.
  • POST /contacts — add a contact.
  • POST /audiences/{audience}/contacts/import — bulk-import contacts from CSV.

Create the campaign

curl https://api.minimailer.app/campaigns \
  -X POST \
  -H "Authorization: Bearer $MINIMAILER_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "campaigns",
      "attributes": {
        "name": "July product update",
        "subject": "What shipped in July",
        "body_html": "<p>...</p>"
      },
      "relationships": {
        "audience": {
          "data": { "type": "audiences", "id": "{audience}" }
        }
      }
    }
  }'

Personalize with merge tags

Write {{name}} anywhere in the subject, HTML body, or text body and each recipient gets their own rendered copy:

{
    "subject": "{{name}}, here is what shipped in July",
    "body_html": "<p>Hi {{name}} — you are on the {{plan}} plan.</p><p><a href=\"{{unsubscribe_url}}\">Unsubscribe</a></p>"
}

Available tags are {{name}}, {{email}}, {{unsubscribe_url}}, {{unsubscribe_mailto}}, and every key you stored in that contact's custom_fields — so {{plan}} above comes from your own import. A tag with no matching value renders as empty rather than failing the send, so a contact missing one field still receives the email. Values are HTML-escaped in body_html, which means imported data can never inject markup into your message; in subject and body_text they are inserted as-is.

The unsubscribe tags are filled in only for real audience contacts. A test_mode send has no contact behind it, so those tags — and any contact field — render empty.

Optional attributes: body_text, from_address, reply_to, and test_mode with test_recipient_email — a test-mode campaign delivers only to the test recipient, so you can proof the real rendering before committing. reply_to follows the same reply-to rule as a transactional send — one address or an array of up to 10, on your sending domain's family — and applies to every message in the campaign.

Drive the lifecycle with PATCH

A campaign is a state machine: draft → scheduled → sending → sent, with paused, cancelled, and failed as side exits. You move it by patching status — set scheduled with a scheduled_at timestamp to send later, or push it straight toward sending. Pause and resume mid-send the same way.

Track progress

Campaign progress fires campaign.sending, campaign.sent, campaign.paused, and campaign.failed webhooks, and GET /campaigns/{campaign}/stats aggregates opens, clicks, bounces, and deliveries per campaign.

Deleting an audience removes its contact list but keeps campaigns that targeted it, including their statistics. A campaign whose audience was deleted no longer includes audience details. An audience cannot be deleted while one of its campaigns is scheduled, sending, or paused; cancel that campaign first.

Every contact you send to counts toward your plan's volume and any free-tier quota, exactly like a transactional recipient — see rate limits and quotas.