API Reference
API Reference
GitHub
Campaigns

Overview

Create and send email campaigns to subscriber lists

A campaign renders one template against every member of a subscriber list and delivers the results. It is bulk mail, and it differs from a batch send in every way that matters: the audience is resolved for you, the send is throttled, it can be paused, and it tracks engagement.

Machine API schema

The /api/v1 surface describes itself: run mailyard export-api-spec and feed the document to Swagger UI, Postman, or a client generator.

Create

POST /api/v1/campaigns
curl -X POST http://localhost:3000/api/v1/campaigns \
  -H "Authorization: Bearer myk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "April dispatch",
    "from_email": "newsletter@example.com",
    "from_name": "Acme Industrial",
    "template_id": "0198f6a1-3c7e-7b21-9f4d-2a5c8e0b1d33",
    "list_id": "0198f6a2-7b19-7d02-9c31-4e8f1a6b3c22",
    "language": "en",
    "template_data": {"month": "April", "featured_url": "https://example.com/april"},
    "send_rate": 600
  }'

Four fields are required — name, from_email, template_id and list_id — and the one most people expect to see among them is not there.

subject is a fallback, not the subject line

The subject comes from the template’s localization, like everything else about the content. The campaign’s subject field is used only when that localization renders an empty one.

So a campaign with a carefully worded subject and a template that has its own will send the template’s. Change the copy where the copy lives.

The rest

FieldDefaultDoes
from_nameDisplay name on the From header
reply_toReply-To address, for a campaign sent from a no-reply mailbox. Not required to be verified
subjectFallback subject, as above
languageLocalization to render. Falls through the usual four steps per subscriber
template_dataCampaign-wide render values, up to 100 keys
smtp_groupproject defaultSlug of the server pool to send through
send_rate0Emails per minute. 0 is unthrottled
send_at_local_timefalseDeliver at the scheduled wall-clock time in each subscriber’s own timezone
ab_test_enabledfalseTurn on A/B testing
ab_variantsUp to 5 variants

smtp_group is worth setting. Bulk mail on its own pool is the usual arrangement, so a campaign that burns an IP’s reputation does not take your transactional mail down with it.

template_data is merged under each subscriber’s own custom fields, and email and name are written last from the subscriber record — so a subscriber field beats a campaign-wide one, and neither can override the recipient’s own address.

Scheduling is not a property of the campaign. Pass scheduled_at to the send call instead — see Sending . A campaign always renders the template’s active version, and there is no way to pin an older one.

Statuses

StatusMeansEditable
draftCreated, never startedYes
scheduledA send was requested for a future timeNo
sendingThe runner is working through the audienceNo
pausedStopped part-way, resumableNo
sentEvery message reached a terminal stateNo
cancelledStopped permanentlyNo

Only draft can be edited, and only draft or scheduled can be sent. Anything else answers 409 with “campaign is already running or finished”.

List

GET /api/v1/campaigns

Every campaign in the project, newest first.

This route takes no parameters

No limit, no offset, no status filter — the whole list comes back in one response, and each entry is the campaign record alone. Per-campaign statistics are not included: those come from the single-campaign route below.

Campaigns are a list somebody made by hand, so it stays small in practice. Filter client-side.

Read one

GET /api/v1/campaigns/{id}

This is the route with the numbers on it:

{
  "campaign": { "id": "...", "name": "April dispatch", "status": "sending" },
  "stats": { "pending": 2100, "queued": 500, "sent": 2300, "failed": 50, "skipped": 50 },
  "stats_by_variant": { "A": { "sent": 1150 }, "B": { "sent": 1150 } },
  "engagement": { "opened": 890, "clicked": 214 }
}

stats counts messages, one per recipient:

  • pending — waiting for a batch
  • queued — an email exists in the delivery queue
  • sent, failed — mirroring that email’s fate
  • skipped — the recipient was suppressed, or the campaign was cancelled before their turn

engagement is the unique recipient view — how many people opened, not how many opens there were. Those counters are aggregated as the send runs, so they survive the tracking-event retention sweep. The per-day series on GET /api/v1/campaigns/{id}/analytics come from the raw event log and only reach back as far as retention keeps it.

Manage

RouteDoes
PATCH /api/v1/campaigns/{id}Edit — draft only
POST /api/v1/campaigns/{id}/duplicateCopy the definition as a fresh draft
POST /api/v1/campaigns/{id}/sendStart, or schedule — see Sending
POST /api/v1/campaigns/{id}/pauseStop between batches
POST /api/v1/campaigns/{id}/resumeCarry on
POST /api/v1/campaigns/{id}/cancelStop for good
GET /api/v1/campaigns/{id}/messagesThe per-recipient rows, with addresses
GET /api/v1/campaigns/{id}/analyticsPer-link click tallies and daily series
DELETE /api/v1/campaigns/{id}Remove the campaign and its messages

Duplicate is the way to iterate: a sent campaign cannot be edited or re-run, so the second attempt is a copy with the audience or the wording changed.