PushMail.dev

Campaigns

Campaigns are one-off email broadcasts sent to a list or tagged group of contacts. Schedule them for later or send immediately.

Sequences vs Campaigns

SequencesCampaigns
TriggerPer contact enrollmentSend to all at once
TimingRelative to enrollment dateFixed date/time
Use caseOnboarding, nurture, educationNewsletters, announcements, promotions

Create a campaign

POST /v1/campaigns

Target a list, a tag, a segment, or a combination. The campaign will send to all subscribed contacts matching the criteria.

Request
curl -X POST https://pushmail.dev/api/v1/campaigns \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "siteId": 1,
    "name": "February Newsletter",
    "templateId": 10,
    "listId": 3,
    "sendingConfigId": 2
  }'
ParameterTypeRequiredDescription
siteIdintegerYesThe site to send from
namestringYesCampaign name
templateIdintegerYesTemplate to use
listIdintegerNoTarget list (one of listId, tagId, or segment)
tagIdintegerNoTarget tag
sendingConfigIdintegerRequired to sendSending config to use. Must be set before sending or scheduling. See Sending for setup.
Response
{
  "data": {
    "id": 5,
    "siteId": 1,
    "name": "February Newsletter",
    "templateId": 10,
    "listId": 3,
    "tagId": null,
    "status": "draft",
    "scheduledAt": null,
    "totalRecipients": 0,
    "sentCount": 0,
    "deliveredCount": 0,
    "openedCount": 0,
    "clickedCount": 0,
    "bouncedCount": 0,
    "complainedCount": 0
  }
}

Schedule a campaign

POST /v1/campaigns/:id/schedule

Schedule the campaign to send at a specific time. The cron worker picks it up and starts processing.

curl -X POST https://pushmail.dev/api/v1/campaigns/5/schedule \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduledAt": "2025-02-01T09:00:00Z"
  }'

Send immediately

POST /v1/campaigns/:id/send

Skip scheduling and send the campaign right now. The campaign must have a listId and templateId.

curl -X POST https://pushmail.dev/api/v1/campaigns/5/send \
  -H "Authorization: Bearer pm_live_YOUR_KEY"
Response
{
  "data": {
    "message": "Campaign is being sent",
    "totalRecipients": 2500
  }
}

Track campaign performance

GET /v1/campaigns/:id

Stats are updated in real-time as provider webhook events arrive.

Response
{
  "data": {
    "campaign": {
      "id": 5,
      "name": "February Newsletter",
      "status": "sent",
      "scheduledAt": "2025-02-01T09:00:00Z",
      "sentAt": "2025-02-01T09:00:12Z",
      "totalRecipients": 2500,
      "sentCount": 2500,
      "deliveredCount": 2431,
      "openedCount": 892,
      "clickedCount": 234,
      "bouncedCount": 69,
      "complainedCount": 3
    }
  }
}

Campaign statuses

StatusDescription
draftCreated but not scheduled or sent
scheduledWaiting for scheduled time
sendingCurrently being sent (may take minutes for large lists)
sentAll emails queued for delivery
ab_testingA/B test sample sent, waiting for test duration to expire
ab_winner_sentWinner selected, remainder sent with winning variant
failedError during send processing

A/B Testing

Test different subject lines, templates, or HTML content against each other. PushMail automatically tracks open and click rates per variant and can pick the winner for you.

How it works

  1. Create a campaign as usual
  2. Add 2-5 variants (each overrides subject, template, or HTML content)
  3. Optionally set a sample percent and duration for automatic winner selection
  4. Send the campaign -- recipients are randomly assigned to variants by weight
  5. After the test duration, the winning variant is sent to the remaining audience

Create a variant

POST /v1/campaigns/:id/variants

Request
curl -X POST https://pushmail.dev/api/v1/campaigns/5/variants \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "variantName": "A",
    "subject": "Big news from our team",
    "weight": 50
  }'
Response
{
  "data": {
    "variant": {
      "id": 1,
      "campaignId": 5,
      "variantName": "A",
      "subject": "Big news from our team",
      "templateId": null,
      "htmlContent": null,
      "weight": 50,
      "sentCount": 0,
      "deliveredCount": 0,
      "openedCount": 0,
      "clickedCount": 0
    }
  }
}
FieldTypeDescription
variantNamestringLabel for the variant (e.g. "A", "B", "Control")
subjectstring?Override subject line (null = use campaign default)
templateIdnumber?Override template (null = use campaign default)
htmlContentstring?Override raw HTML body (null = use campaign default)
weightnumberPercentage of audience assigned to this variant (1-100)

List variants

GET /v1/campaigns/:id/variants

curl https://pushmail.dev/api/v1/campaigns/5/variants \
  -H "Authorization: Bearer pm_live_YOUR_KEY"

Update a variant

PUT /v1/campaigns/:id/variants/:variantId

curl -X PUT https://pushmail.dev/api/v1/campaigns/5/variants/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Updated subject line",
    "weight": 60
  }'

Delete a variant

DELETE /v1/campaigns/:id/variants/:variantId

Deleting all variants automatically disables A/B testing on the campaign.

Configure A/B test settings

PUT /v1/campaigns/:id

Set the winner selection criteria on the campaign itself:

curl -X PUT https://pushmail.dev/api/v1/campaigns/5 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "abTestWinnerMetric": "open_rate",
    "abTestSamplePercent": 20,
    "abTestDurationMinutes": 240
  }'
FieldTypeDescription
abTestWinnerMetricstring"open_rate" or "click_rate"
abTestSamplePercentnumber% of audience for the initial test (10-50, rest gets winner)
abTestDurationMinutesnumberHow long to wait before picking the winner (30-1440)

Send an A/B test campaign

POST /v1/campaigns/:id/send

When abTestSamplePercent is set, only that percentage of recipients receive the test. After the duration expires, the cron worker picks the winner and sends to the rest.

When abTestSamplePercent is not set (or is 100), all recipients are sent to immediately with random variant assignment -- useful for pure split testing without automatic winner selection.

Response (sample test)
{
  "data": {
    "message": "A/B test started \u2014 sample group is being sent",
    "totalRecipients": 10000,
    "sampleRecipients": 2000,
    "remainderRecipients": 8000,
    "durationMinutes": 240,
    "winnerMetric": "open_rate",
    "variants": [
      { "id": 1, "name": "A", "weight": 50 },
      { "id": 2, "name": "B", "weight": 50 }
    ]
  }
}

View variant performance

GET /v1/campaigns/:id

When a campaign has A/B testing enabled, the response includes a variants array with computed open and click rates:

Response
{
  "data": {
    "campaign": {
      "id": 5,
      "name": "February Newsletter",
      "status": "ab_winner_sent",
      "abTestEnabled": true,
      "abTestWinnerMetric": "open_rate",
      "abTestWinnerVariantId": 1,
      "abTestSamplePercent": 20,
      "abTestDurationMinutes": 240
    },
    "variants": [
      {
        "id": 1,
        "variantName": "A",
        "subject": "Big news from our team",
        "weight": 50,
        "sentCount": 1000,
        "deliveredCount": 980,
        "openedCount": 245,
        "clickedCount": 62,
        "openRate": 24.5,
        "clickRate": 6.2
      },
      {
        "id": 2,
        "variantName": "B",
        "subject": "You will not believe this update",
        "weight": 50,
        "sentCount": 1000,
        "deliveredCount": 975,
        "openedCount": 198,
        "clickedCount": 41,
        "openRate": 19.8,
        "clickRate": 4.1
      }
    ]
  }
}

Full example: A/B test subject lines

ab-test-subject.ts
const API = "https://pushmail.dev/api/v1";
const KEY = process.env.PUSHMAIL_KEY;

async function abTestSubjectLines(campaignId: number) {
  const headers = {
    "Authorization": `Bearer ${KEY}`,
    "Content-Type": "application/json",
  };

  // 1. Add variant A
  await fetch(`${API}/campaigns/${campaignId}/variants`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      variantName: "A",
      subject: "Your weekly digest is here",
      weight: 50,
    }),
  });

  // 2. Add variant B
  await fetch(`${API}/campaigns/${campaignId}/variants`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      variantName: "B",
      subject: "5 things you missed this week",
      weight: 50,
    }),
  });

  // 3. Configure: test 20% for 4 hours, pick winner by open rate
  await fetch(`${API}/campaigns/${campaignId}`, {
    method: "PUT",
    headers,
    body: JSON.stringify({
      abTestWinnerMetric: "open_rate",
      abTestSamplePercent: 20,
      abTestDurationMinutes: 240,
    }),
  });

  // 4. Send -- 20% gets the test, 80% gets the winner after 4 hours
  await fetch(`${API}/campaigns/${campaignId}/send`, {
    method: "POST",
    headers,
  });
}

Full example: Weekly newsletter

send-newsletter.ts
const API = "https://pushmail.dev/api/v1";
const KEY = process.env.PUSHMAIL_KEY;
const SITE_ID = 1;
const NEWSLETTER_LIST = 3;

async function sendWeeklyNewsletter(templateId: number) {
  const headers = {
    "Authorization": `Bearer ${KEY}`,
    "Content-Type": "application/json",
  };

  // 1. Create campaign
  const campaign = await fetch(`${API}/campaigns`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      siteId: SITE_ID,
      name: `Newsletter - ${new Date().toISOString().split("T")[0]}`,
      templateId,
      listId: NEWSLETTER_LIST,
    }),
  }).then(r => r.json());

  // 2. Send immediately
  await fetch(`${API}/campaigns/${campaign.data.id}/send`, {
    method: "POST",
    headers,
  });

  console.log(`Campaign ${campaign.data.id} sent!`);
}

Next steps

  • Segments -- Target dynamic rule-based audiences in campaigns
  • Sending -- Configure BYOK keys and delivery settings
  • Webhooks -- Get real-time event notifications

On this page