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
| Sequences | Campaigns | |
|---|---|---|
| Trigger | Per contact enrollment | Send to all at once |
| Timing | Relative to enrollment date | Fixed date/time |
| Use case | Onboarding, nurture, education | Newsletters, 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.
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
}'| Parameter | Type | Required | Description |
|---|---|---|---|
siteId | integer | Yes | The site to send from |
name | string | Yes | Campaign name |
templateId | integer | Yes | Template to use |
listId | integer | No | Target list (one of listId, tagId, or segment) |
tagId | integer | No | Target tag |
sendingConfigId | integer | Required to send | Sending config to use. Must be set before sending or scheduling. See Sending for setup. |
{
"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"{
"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.
{
"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
| Status | Description |
|---|---|
draft | Created but not scheduled or sent |
scheduled | Waiting for scheduled time |
sending | Currently being sent (may take minutes for large lists) |
sent | All emails queued for delivery |
ab_testing | A/B test sample sent, waiting for test duration to expire |
ab_winner_sent | Winner selected, remainder sent with winning variant |
failed | Error 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
- Create a campaign as usual
- Add 2-5 variants (each overrides subject, template, or HTML content)
- Optionally set a sample percent and duration for automatic winner selection
- Send the campaign -- recipients are randomly assigned to variants by weight
- After the test duration, the winning variant is sent to the remaining audience
Create a variant
POST /v1/campaigns/:id/variants
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
}'{
"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
}
}
}| Field | Type | Description |
|---|---|---|
variantName | string | Label for the variant (e.g. "A", "B", "Control") |
subject | string? | Override subject line (null = use campaign default) |
templateId | number? | Override template (null = use campaign default) |
htmlContent | string? | Override raw HTML body (null = use campaign default) |
weight | number | Percentage 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
}'| Field | Type | Description |
|---|---|---|
abTestWinnerMetric | string | "open_rate" or "click_rate" |
abTestSamplePercent | number | % of audience for the initial test (10-50, rest gets winner) |
abTestDurationMinutes | number | How 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.
{
"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:
{
"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
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
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!`);
}