MailLandDocs

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:

StatusValueDescription
Draft0Initial state: editable, not yet sent
Scheduled1Queued for future delivery
Sending2Currently being sent to recipients
Sent3All recipients processed
Paused4Sending paused mid-flight
Cancelled5Sending cancelled
Failed6System error during send

Allowed transitions:

  • Draft → Scheduled (schedule) or Sending (send immediately) or Cancelled (cancel)
  • Scheduled → Sending or Cancelled: rescheduling a Scheduled campaign is not supported
  • Sending → Sent, Paused, Cancelled, or Failed
  • Paused → Sending (resume)
  • Sent or Cancelled → archived

Campaign Types

TypeValueDescription
Regular0Standard one-time campaign
Automated1Triggered by automations
ABTest2A/B test campaign

Personalization

Campaign subject lines and content support {{variable}} merge tags, substituted per recipient from that subscriber's own record.

TagValue
{{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>}}
Personalized campaign content
<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
Create campaign request
{
  "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"
}
FieldTypeRequiredDescription
namestringYesCampaign name
subjectstringYesEmail subject line
previewTextstringNoPreview text shown in inbox
fromNamestringYesSender display name
fromEmailstringYesSender email address
replyTostringNoReply-to address
htmlContentstringYesHTML email body
textContentstringNoPlain text fallback
templateIdstringNoTemplate ID to use
typeintNoCampaign type (default: 0 Regular)
mailingListIdstringNo*Target mailing list
segmentIdstringNo*Target segment

* Provide mailingListId or segmentId: one of the two is required unless templateId is set.

Create a campaign
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

Campaign 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
ParameterTypeDefaultDescription
cursorstring-Opaque cursor from the previous page's nextCursor. Omit for the first page.
limitint20Items per page (max 100)
searchstring-Search by name
statusint-Filter by CampaignStatus
typeint-Filter by CampaignType
mailingListIdstring-Filter by mailing list
sortBystring-Sort field
sortDescboolfalseSort descending
includeArchivedboolfalseInclude archived campaigns
List campaigns
curl "https://api.mail.land/v1/marketing/campaigns?limit=20&status=3" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Campaign list response
{
  "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:

Get campaign details
curl https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Campaign detail response
{
  "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

RateFormula
deliveryRatetotalDelivered / totalSent * 100
openRatetotalOpened / totalDelivered * 100
clickRatetotalClicked / totalDelivered * 100
clickToOpenRatetotalClicked / totalOpened * 100
bounceRatetotalBounced / totalSent * 100
unsubscribeRatetotalUnsubscribed / 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.

StatusCodeDescription
200-Campaign updated
400INVALID_REQUESTOnly draft campaigns can be edited
404RESOURCE_NOT_FOUNDCampaign not found
Update a campaign
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
Schedule request
{
  "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.

StatusCodeDescription
200-Campaign scheduled
400VALIDATION_FAILEDScheduled time must be in the future
404RESOURCE_NOT_FOUNDCampaign not found or cannot be scheduled
403MARKETING_NOT_YET_ALLOWEDMarketing is not open for the account yet (see Error Codes); /send returns it too
403MARKETING_SUSPENDEDMarketing is unavailable platform-wide; /send returns it too
422CAMPAIGN_LINK_BLOCKEDA link in the campaign points at a destination flagged as unsafe; /send returns it too. Nothing is scheduled or sent
Schedule a campaign
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}/send

Sends 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).

Send a campaign immediately
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}/pause
Pause a campaign
curl -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}/resume
Resume a campaign
curl -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}/cancel

Cancels a Draft, Scheduled or Sending campaign. Already-sent emails are not recalled.

Cancel a campaign
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%20Newsletter

Creates a new Draft campaign with all settings copied from the original. The name query parameter is optional.

Duplicate a campaign
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}/archive
Archive a campaign
curl -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}/unarchive
Unarchive a campaign
curl -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
ParameterTypeDefaultDescription
cursorstring-Opaque cursor from the previous page's nextCursor. Omit for the first page.
limitint20Items per page (max 100)
statusstring-Filter by recipient status
searchstring-Search by email
Get campaign recipients
curl "https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w/recipients?limit=20" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Campaign recipients response
{
  "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

StatusValueDescription
Pending0Queued, not yet sent
Sent1Accepted by MTA
Delivered2Delivered to recipient
Opened3Recipient opened
Clicked4Recipient clicked a link
Bounced5Hard bounce
Unsubscribed6Recipient unsubscribed
Complained7Spam complaint
Failed8Delivery 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.

Delete a campaign
curl -X DELETE https://api.mail.land/v1/marketing/campaigns/uivozyvucowj9w \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Error Responses

StatusCodeDescription
400VALIDATION_FAILEDMissing required fields
400INVALID_REQUESTOnly draft campaigns can be edited
403FORBIDDENInsufficient permissions
404RESOURCE_NOT_FOUNDCampaign 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

On this page