Campaigns
Create, schedule, and send email marketing campaigns with tracking and analytics
Create and manage email marketing campaigns. MailLand handles recipient selection, sending, delivery tracking, and engagement analytics for each campaign.
Campaign Lifecycle
Every campaign moves through a defined status flow:
| Status | Value | Description |
|---|---|---|
Draft | 0 | Initial state: editable, not yet sent |
Scheduled | 1 | Queued for future delivery |
Sending | 2 | Currently being sent to recipients |
Sent | 3 | All recipients processed |
Paused | 4 | Sending paused mid-flight |
Cancelled | 5 | Sending cancelled |
Failed | 6 | System error during send |
Allowed transitions:
Draft→Scheduled(schedule) orSending(send immediately) orCancelled(cancel)Scheduled→SendingorCancelled: rescheduling aScheduledcampaign is not supportedSending→Sent,Paused,Cancelled, orFailedPaused→Sending(resume)SentorCancelled→ archived
Campaign Types
| Type | Value | Description |
|---|---|---|
Regular | 0 | Standard one-time campaign |
Automated | 1 | Triggered by automations |
ABTest | 2 | A/B test campaign |
Personalization
Campaign subject lines and content support {{variable}} merge tags, substituted per recipient
from that subscriber's own record.
| Tag | Value |
|---|---|
{{firstName}} | Subscriber's first name |
{{lastName}} | Subscriber's last name |
{{fullName}} | First and last name joined |
{{email}} | Subscriber's email address |
{{subscriberId}} | Opaque subscriber identifier |
{{custom_<field>}} | Any custom field on the subscriber, also available as {{<field>}} |
<p>Hi {{firstName}}, here's what's new this month.</p>A tag with no value is left exactly as written: {{firstName}} for a subscriber with no first
name stays {{firstName}} in the delivered email rather than becoming an empty gap. Write content
that reads correctly either way, e.g. Hi there{{firstName}} is a poor fit but "Your account" is
safe. Use a fallback greeting when you are not sure every subscriber has the field.
Values are escaped for the part they land in: HTML content is HTML-escaped (so a name containing
& or < cannot break your markup), while the subject line and plain-text body are never
escaped, Ben & Jerry's stays exactly that.
The older snake_case spellings ({{first_name}}, {{last_name}}, {{full_name}},
{{subscriber_id}}) continue to work as aliases.
Create a Campaign
The workspace must be Business Verified before you can create or update a campaign: an earlier,
separate gate from the fromEmail check below. If it is not, the request fails with 400 INVALID_REQUEST ("Marketing campaigns require Business Verified status…").
The fromEmail domain must already be added and fully verified (SPF + DKIM + DMARC) at create and update time, or the request is rejected (400/404). This check happens here, not at send time, see Send Immediately.
POST https://api.mail.land/v1/marketing/campaigns{
"name": "February Newsletter",
"subject": "What's new in February",
"previewText": "Product updates and tips",
"fromName": "MailLand Team",
"fromEmail": "[email protected]",
"replyTo": "[email protected]",
"htmlContent": "<html>...</html>",
"textContent": "Plain text version...",
"type": 0,
"mailingListId": "ddv6ou6kz4wbto",
"segmentId": "x4xa3ep1zmqq0b"
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Campaign name |
subject | string | Yes | Email subject line |
previewText | string | No | Preview text shown in inbox |
fromName | string | Yes | Sender display name |
fromEmail | string | Yes | Sender email address |
replyTo | string | No | Reply-to address |
htmlContent | string | Yes | HTML email body |
textContent | string | No | Plain text fallback |
templateId | string | No | Template ID to use |
type | int | No | Campaign type (default: 0 Regular) |
mailingListId | string | No* | Target mailing list |
segmentId | string | No* | Target segment |
* Provide mailingListId or segmentId: one of the two is required unless templateId is set.
curl -X POST https://api.mail.land/v1/marketing/campaigns \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "February Newsletter",
"subject": "What'\''s new in February",
"fromName": "MailLand Team",
"fromEmail": "[email protected]",
"htmlContent": "<html><body><h1>Hello!</h1></body></html>",
"mailingListId": "ddv6ou6kz4wbto"
}'Response
{
"id": "uivozyvucowj9w",
"name": "February Newsletter",
"subject": "What's new in February",
"previewText": "Product updates and tips",
"fromName": "MailLand Team",
"fromEmail": "[email protected]",
"status": "draft",
"statusName": "Draft",
"type": 0,
"typeName": "Regular",
"mailingListId": "ddv6ou6kz4wbto",
"mailingListName": "Main Newsletter",
"segmentId": null,
"segmentName": null,
"createdAt": "2026-02-17T12:00:00Z",
"updatedAt": "2026-02-17T12:00:00Z"
}List Campaigns
GET https://api.mail.land/v1/marketing/campaigns| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | - | Opaque cursor from the previous page's nextCursor. Omit for the first page. |
limit | int | 20 | Items per page (max 100) |
search | string | - | Search by name |
status | int | - | Filter by CampaignStatus |
type | int | - | Filter by CampaignType |
mailingListId | string | - | Filter by mailing list |
sortBy | string | - | Sort field |
sortDesc | bool | false | Sort descending |
includeArchived | bool | false | Include archived campaigns |
curl "https://api.mail.land/v1/marketing/campaigns?limit=20&status=3" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"items": [
{
"id": "uivozyvucowj9w",
"name": "February Newsletter",
"subject": "What's new in February",
"status": "sent",
"statusName": "Sent",
"type": 0,
"typeName": "Regular",
"mailingListId": "ddv6ou6kz4wbto",
"mailingListName": "Main Newsletter",
"totalRecipients": 5000,
"totalSent": 4980,
"totalDelivered": 4900,
"totalOpened": 1520,
"totalClicked": 195,
"totalBounced": 45,
"totalUnsubscribed": 12,
"sentAt": "2026-02-15T10:00:00Z",
"completedAt": "2026-02-15T10:35:00Z",
"createdAt": "2026-02-14T08:00:00Z"
}
],
"nextCursor": "eyJ2IjoxLCJzIjoiY3JlYXRlZGF0OmRlc2MiLCJrIjoi...",
"hasMore": true
}Get Campaign Details
GET https://api.mail.land/v1/marketing/campaigns/{id}Returns the full CampaignDetailDto including HTML content, all stats, and computed engagement rates:
curl https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"id": "uivozyvucowj9w",
"name": "February Newsletter",
"subject": "What's new in February",
"previewText": "Product updates and tips",
"fromName": "MailLand Team",
"fromEmail": "[email protected]",
"replyTo": "[email protected]",
"status": "sent",
"statusName": "Sent",
"type": 0,
"typeName": "Regular",
"htmlContent": "<html>...</html>",
"textContent": "Plain text...",
"mailingListId": "ddv6ou6kz4wbto",
"mailingListName": "Main Newsletter",
"totalRecipients": 5000,
"totalSent": 4980,
"totalDelivered": 4900,
"totalOpened": 1520,
"totalClicked": 195,
"totalBounced": 45,
"totalUnsubscribed": 12,
"totalComplaints": 2,
"deliveryRate": 98.39,
"openRate": 31.02,
"clickRate": 3.98,
"clickToOpenRate": 12.83,
"bounceRate": 0.9,
"unsubscribeRate": 0.24,
"openTrackingEnabled": true,
"clickTrackingEnabled": true,
"sentAt": "2026-02-15T10:00:00Z",
"completedAt": "2026-02-15T10:35:00Z",
"createdAt": "2026-02-14T08:00:00Z",
"updatedAt": "2026-02-15T10:35:00Z"
}Engagement Rates
| Rate | Formula |
|---|---|
deliveryRate | totalDelivered / totalSent * 100 |
openRate | totalOpened / totalDelivered * 100 |
clickRate | totalClicked / totalDelivered * 100 |
clickToOpenRate | totalClicked / totalOpened * 100 |
bounceRate | totalBounced / totalSent * 100 |
unsubscribeRate | totalUnsubscribed / totalDelivered * 100 |
Rates are calculated on the server from the raw counters (opens and clicks are unique per recipient). deliveryRate and bounceRate are rates of sent; openRate, clickRate and unsubscribeRate are rates of delivered; clickToOpenRate (CTOR) is clickers over openers and is a separate metric from clickRate. A rate is a percentage that is not capped (clickToOpenRate can exceed 100) and is null, never 0, when it is unavailable: its denominator is 0, or the account has the tracking it needs switched off (openTrackingEnabled / clickTrackingEnabled show the current setting). To combine campaigns, add the counters and divide once; do not average percentages. The same fields are returned for each campaign in the list.
Update a Campaign
PUT https://api.mail.land/v1/marketing/campaigns/{id}Only Draft or Scheduled campaigns can be updated. The request body matches the create request,
except type, which cannot be changed.
| Status | Code | Description |
|---|---|---|
200 | - | Campaign updated |
400 | INVALID_REQUEST | Only draft campaigns can be edited |
404 | RESOURCE_NOT_FOUND | Campaign not found |
curl -X PUT https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "February Newsletter (Updated)",
"subject": "What'\''s new in February - updated",
"fromName": "MailLand Team",
"fromEmail": "[email protected]",
"htmlContent": "<html><body><h1>Updated!</h1></body></html>",
"mailingListId": "ddv6ou6kz4wbto"
}'Schedule a Campaign
POST https://api.mail.land/v1/marketing/campaigns/{id}/schedule{
"scheduledAt": "2026-02-20T14:00:00Z"
}The scheduledAt time must be in the future. The campaign status changes to Scheduled and the CampaignWorker picks it up at the scheduled time.
Scheduling is only allowed from Draft status: an already-Scheduled campaign cannot be rescheduled through this endpoint.
| Status | Code | Description |
|---|---|---|
200 | - | Campaign scheduled |
400 | VALIDATION_FAILED | Scheduled time must be in the future |
404 | RESOURCE_NOT_FOUND | Campaign not found or cannot be scheduled |
403 | MARKETING_NOT_YET_ALLOWED | Marketing is not open for the account yet (see Error Codes); /send returns it too |
403 | MARKETING_SUSPENDED | Marketing is unavailable platform-wide; /send returns it too |
422 | CAMPAIGN_LINK_BLOCKED | A link in the campaign points at a destination flagged as unsafe; /send returns it too. Nothing is scheduled or sent |
curl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/schedule \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{ "scheduledAt": "2026-02-20T14:00:00Z" }'Send Immediately
POST https://api.mail.land/v1/marketing/campaigns/{id}/sendSends the campaign immediately. The campaign status transitions to Sending, and the CampaignWorker begins processing recipients.
/send itself does not check domain verification: that check happens at create/update time (see Create a Campaign). /send returns 422 CAMPAIGN_NO_ELIGIBLE_RECIPIENTS if nobody in the target list/segment is eligible to receive the campaign (e.g. all suppressed or opted out).
curl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/send \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Pause and Resume
Pause a campaign that is currently sending:
POST https://api.mail.land/v1/marketing/campaigns/{id}/pausecurl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/pause \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Resume a paused campaign:
POST https://api.mail.land/v1/marketing/campaigns/{id}/resumecurl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/resume \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Pausing stops the CampaignWorker from processing remaining recipients. Already-sent emails are not affected. Resuming continues from where it left off.
Cancel a Campaign
POST https://api.mail.land/v1/marketing/campaigns/{id}/cancelCancels a Draft, Scheduled or Sending campaign. Already-sent emails are not recalled.
curl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/cancel \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Duplicate a Campaign
POST https://api.mail.land/v1/marketing/campaigns/{id}/duplicate?name=Copy%20of%20NewsletterCreates a new Draft campaign with all settings copied from the original. The name query parameter is optional.
curl -X POST "https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/duplicate?name=Copy%20of%20Newsletter" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Archive and Unarchive
Archive a Sent or Cancelled campaign:
POST https://api.mail.land/v1/marketing/campaigns/{id}/archivecurl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/archive \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Unarchive an archived campaign:
POST https://api.mail.land/v1/marketing/campaigns/{id}/unarchivecurl -X POST https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/unarchive \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Archived campaigns are hidden from the default list view unless includeArchived=true is passed.
Campaign Recipients
Get paginated recipients with delivery status for a campaign:
GET https://api.mail.land/v1/marketing/campaigns/{id}/recipients| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | - | Opaque cursor from the previous page's nextCursor. Omit for the first page. |
limit | int | 20 | Items per page (max 100) |
status | string | - | Filter by recipient status |
search | string | - | Search by email |
curl "https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/recipients?limit=20" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"items": [
{
"email": "[email protected]",
"status": "Opened",
"sentAt": "2026-02-15T10:00:30Z",
"deliveredAt": "2026-02-15T10:00:32Z",
"openedAt": "2026-02-15T11:15:00Z",
"clickedAt": "2026-02-15T11:16:30Z",
"bouncedAt": null,
"complainedAt": null,
"unsubscribedAt": null,
"openCount": 2,
"clickCount": 1,
"bounceReason": null
}
],
"nextCursor": "eyJ2IjoxLCJzIjoiY3JlYXRlZGF0OmRlc2MiLCJrIjoi...",
"hasMore": true
}Recipient Statuses
| Status | Value | Description |
|---|---|---|
Pending | 0 | Queued, not yet sent |
Sent | 1 | Accepted by MTA |
Delivered | 2 | Delivered to recipient |
Opened | 3 | Recipient opened |
Clicked | 4 | Recipient clicked a link |
Bounced | 5 | Hard bounce |
Unsubscribed | 6 | Recipient unsubscribed |
Complained | 7 | Spam complaint |
Failed | 8 | Delivery failed |
openedAt/openCount and clickedAt/clickCount include machine activity, Apple Mail Privacy Protection prefetch and security link scanners, which MailLand does not filter out today. Treat open/click-derived rates as an upper bound, not a precise human-engagement number.
Delete a Campaign
DELETE https://api.mail.land/v1/marketing/campaigns/{id}Permanently deletes a campaign and its recipient data. Only Draft or Scheduled campaigns can be deleted; deleting any other status returns 404, archive a sent campaign instead.
curl -X DELETE https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_FAILED | Missing required fields |
400 | INVALID_REQUEST | Only draft campaigns can be edited |
403 | FORBIDDEN | Insufficient permissions |
404 | RESOURCE_NOT_FOUND | Campaign not found |
Next Steps
- Subscribers: manage your subscriber contacts
- Lists: organize subscribers into mailing lists
- Segments: target campaigns with dynamic segments
- Automations: set up trigger-based email sequences