Newsletter

Newsletters API

7 endpoints under https://api.mailneo.co/api/v2. Every request carries your key in the x-api-key header.

Scopes used on this page: newsletters:read, newsletters:send, newsletters:write. Each endpoint below states the one it requires. Start with the quickstart →

GET/newsletters

List newsletters

Requires newsletters:read

Newsletter campaigns — both one-off broadcasts and drip sequences — with their delivery and engagement counters. Cursor-paginated; pass updated_since with the synced_through value from your previous run to fetch only what changed.

The message body is not on this response: it is customer-authored HTML and frequently tens of kilobytes. Retrieve a single newsletter to get body and alt_body.

Query parameters9
limitintegerdefault: 251..100
cursorstring≤ 256 chars
Opaque page cursor from a previous response’s next_cursor. Treat it as a black box: cursors are bound to the sort they were issued under and are validated strictly, so a hand-crafted, modified or foreign cursor — including any cursor issued before the 2026-08 format revision — answers 400 cursor_invalid. On that error, restart from the first page.
order"asc" | "desc"default: desc
updated_sincestring · date-time
sort"updated_at" | "created_at"default: updated_at
status"DRAFT" | "SENDING" | "SCHEDULED" | "PAUSED" | "FAILED" | "SENT" | "ACTIVE" | "CANCELLED"
type"REGULAR" | "OPTIN"
campaign_mode"broadcast" | "drip"
namestring≤ 200 chars
curl "https://api.mailneo.co/api/v2/newsletters?limit=25&order=desc&sort=updated_at" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": [
    {
      "object": "newsletter",
      "id": "c11vl0ia125km7911blt4c11l",
      "name": "April product notes",
      "subject": "April product notes",
      "preheader": "What shipped this month",
      "from_email": "hello@example.com",
      "from_name": "Acme Updates",
      "reply_to": "hello@example.com",
      "content_type": "text/html",
      "status": "DRAFT",
      "type": "…",
      "campaign_mode": "…",
      "is_drip": false,
      "tags": [
        "vip"
      ],
      "send_at": "2026-06-24T15:45:00.000Z",
      "started_at": "2026-05-11T09:30:00.000Z",
      "archive": true,
      "archive_slug": "april-product-notes",
      "recipient_count": 1250,
      "sent_count": 128,
      "open_count": 128,
      "unique_open_count": 128,
      "click_count": 128,
      "unique_click_count": 128,
      "unsubscribe_count": 128,
      "created_at": "2026-05-11T09:30:00.000Z",
      "updated_at": "2026-06-24T15:45:00.000Z"
    }
  ],
  "meta": {
    "request_id": "59e7ecbb-58e7-4b28-abe7-efe15ae7ee4e",
    "has_more": false,
    "next_cursor": null,
    "synced_through": "2026-06-24T15:45:00.000Z"
  }
}
POST/newsletters

Create a newsletter draft

Requires newsletters:write · Idempotency-Key optional

Creates one newsletter, always as a one-off broadcast DRAFT — this endpoint cannot send, schedule or arm anything. status, send_at (and the v1 spelling sendAt) and send_days are refused by name with a 400 (never silently ignored): scheduling or starting a newsletter is newsletters:send, a separate scope with its own two-step preview/confirm flow (POST /newsletters/{newsletterId}/send-preview). The drip-sequence fields (is_drip, campaign_mode, drip_auto_enroll) are likewise refused by name — drip sequences are not creatable through this API.

email_account_id must be the id of an ACTIVE sending account in your team; an unknown, inactive or foreign id fails the write with a 404 naming it. It is stored but never echoed back — reading sending-account identity is accounts:read.

content_type defaults to HTML (you are supplying rendered HTML in body); the audience is attached in the app for now — list targeting through the API is planned as its own sub-resource. Names are not required to be unique: two newsletters may share one and are told apart by id.

Query parameters1
dry_runboolean
Report what this call WOULD do and commit nothing. Returns data: null and a meta.impact block. Requires the resource's :read scope in addition to the scope this endpoint declares. Must be exactly true or false.
Header parameters1
Idempotency-Keystring≤ 255 chars
A caller-chosen unique string, at most 255 characters. Retrying with the same key replays the first response instead of repeating the work; the replay carries Idempotent-Replayed: true. Reusing a key with a different body is a 409 idempotency_key_reused. Keys are remembered for 24 hours.
Body parameters22
namestringrequired1..200 chars
subjectstringrequired1..5000 chars
from_emailstringrequired3..320 chars
bodystringrequired1..524288 chars
preheaderstring · nullable≤ 5000 chars
from_namestring · nullable≤ 200 chars
reply_tostring · nullable3..320 chars
alt_bodystring · nullable≤ 524288 chars
content_type"RICHTEXT" | "HTML" | "PLAIN" | "VISUAL"
tagsstring[]≤ 50 items
email_account_idstring · nullable1..64 chars
statusany
send_atany
sendAtany
send_daysany
sendDaysany
is_dripany
isDripany
campaign_modeany
campaignModeany
drip_auto_enrollany
dripAutoEnrollany
curl -X POST "https://api.mailneo.co/api/v2/newsletters" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "subject": "April product notes",
    "from_email": "hello@example.com",
    "body": "<p>Hi {{first_name}},</p>",
    "preheader": "What shipped this month",
    "from_name": "Acme Updates",
    "reply_to": "hello@example.com",
    "alt_body": "Hi {{first_name}},"
  }'
{
  "data": "…",
  "meta": {
    "request_id": "aa54d8fc-ab54-4a8f-ac54-dc22ad54ddb5",
    "dry_run": false
  }
}
GET/newsletters/{newsletterId}

Retrieve a newsletter

Requires newsletters:read

Includes body and alt_body, which the list response omits.

Path parameters1
newsletterIdstringrequired1..64 chars
curl "https://api.mailneo.co/api/v2/newsletters/{newsletterId}" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": {
    "object": "newsletter",
    "id": "c11vl0ia125km7911blt4c11l",
    "name": "April product notes",
    "subject": "April product notes",
    "preheader": "What shipped this month",
    "from_email": "hello@example.com",
    "from_name": "Acme Updates",
    "reply_to": "hello@example.com",
    "content_type": "text/html",
    "status": "DRAFT",
    "type": "…",
    "campaign_mode": "…",
    "is_drip": false,
    "tags": [
      "vip"
    ],
    "send_at": "2026-06-24T15:45:00.000Z",
    "started_at": "2026-05-11T09:30:00.000Z",
    "archive": true,
    "archive_slug": "april-product-notes",
    "recipient_count": 1250,
    "sent_count": 128,
    "open_count": 128,
    "unique_open_count": 128,
    "click_count": 128,
    "unique_click_count": 128,
    "unsubscribe_count": 128,
    "created_at": "2026-05-11T09:30:00.000Z",
    "updated_at": "2026-06-24T15:45:00.000Z",
    "body": "<p>Hi {{first_name}},</p>",
    "alt_body": "Hi {{first_name}},"
  },
  "meta": {
    "request_id": "27620f44-2862-40d7-a962-126a2a6213fd"
  }
}
PATCH/newsletters/{newsletterId}

Update a newsletter draft

Requires newsletters:write · Idempotency-Key optional

Merge-patch: fields you omit are left alone, and null clears preheader, reply_to or alt_body (from_name resets to ""). Only a DRAFT newsletter can be edited — anything else is a 409 resource_state_invalid: a SCHEDULED newsletter is armed and editing it would retime or reshape a pending send, SENDING/PAUSED are mid-send, ACTIVE is a live drip sequence, and SENT/FAILED/CANCELLED are the record of what was actually sent. Un-schedule or stop it in the app first.

status, send_at/sendAt and send_days are refused by name with a 400 — arming a send is newsletters:send, via its two-step preview/confirm flow (POST /newsletters/{newsletterId}/send-preview); un-arming is an app action. The drip-sequence fields (is_drip, campaign_mode, drip_auto_enroll) are refused the same way.

Path parameters1
newsletterIdstringrequired1..64 chars
Query parameters1
dry_runboolean
Report what this call WOULD do and commit nothing. Returns data: null and a meta.impact block. Requires the resource's :read scope in addition to the scope this endpoint declares. Must be exactly true or false.
Header parameters1
Idempotency-Keystring≤ 255 chars
A caller-chosen unique string, at most 255 characters. Retrying with the same key replays the first response instead of repeating the work; the replay carries Idempotent-Replayed: true. Reusing a key with a different body is a 409 idempotency_key_reused. Keys are remembered for 24 hours.
Body parameters22
namestring1..200 chars
subjectstring1..5000 chars
from_emailstring3..320 chars
bodystring1..524288 chars
preheaderstring · nullable≤ 5000 chars
from_namestring · nullable≤ 200 chars
reply_tostring · nullable3..320 chars
alt_bodystring · nullable≤ 524288 chars
content_type"RICHTEXT" | "HTML" | "PLAIN" | "VISUAL"
tagsstring[]≤ 50 items
email_account_idstring · nullable1..64 chars
statusany
send_atany
sendAtany
send_daysany
sendDaysany
is_dripany
isDripany
campaign_modeany
campaignModeany
drip_auto_enrollany
dripAutoEnrollany
curl -X PATCH "https://api.mailneo.co/api/v2/newsletters/{newsletterId}" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "subject": "April product notes",
    "from_email": "hello@example.com",
    "body": "<p>Hi {{first_name}},</p>",
    "preheader": "What shipped this month",
    "from_name": "Acme Updates",
    "reply_to": "hello@example.com",
    "alt_body": "Hi {{first_name}},"
  }'
{
  "data": "…",
  "meta": {
    "request_id": "9d747eb5-9c74-4d22-ab74-7b8f9a7479fc",
    "dry_run": false
  }
}
DELETE/newsletters/{newsletterId}

Delete a newsletter draft

Requires newsletters:write

Permanent, and there is no undo — call with ?dry_run=true first. Only a DRAFT that has never sent mail can be deleted; anything else is a 409 resource_state_invalid.

The restriction exists because deletion cascades into the engagement record: every open, click and view event row goes with the newsletter, its per-recipient send log is orphaned, and bounce records are detached. For a newsletter that has actually sent — including a partially-sent CANCELLED or FAILED one — that is the audit and analytics record of mail real people received, and this API will not destroy it. The app can; this surface is deliberately narrower.

What a draft delete removes besides the newsletter row: its issues, its list attachments and any drip-sequence steps.

Path parameters1
newsletterIdstringrequired1..64 chars
Query parameters1
dry_runboolean
Report what this call WOULD do and commit nothing. Returns data: null and a meta.impact block. Requires the resource's :read scope in addition to the scope this endpoint declares. Must be exactly true or false.
curl -X DELETE "https://api.mailneo.co/api/v2/newsletters/{newsletterId}" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": {
    "object": "newsletter",
    "id": "c11vl0ia125km7911blt4c11l",
    "deleted": true
  },
  "meta": {
    "request_id": "0881d38b-0781-41f8-aa81-d6b10981d51e",
    "dry_run": false
  }
}
POST/newsletters/{newsletterId}/send-preview

Preview a newsletter send and get a confirmation token

Requires newsletters:send · Idempotency-Key required

Step 1 of 2. Resolves exactly what a send would do — the subscribers of the newsletter's lists (narrowed by any segment targeting) remaining after opt-in and suppression filtering, credits required, the sending identity, daily-cap headroom — runs every send gate read-only, and returns the impact plus a single-use confirmation token bound to that resolved plan. Nothing is scheduled or sent by this call.

Broadcasts only: a drip sequence is refused (activating one enrols subscribers perpetually, which a one-shot confirmation cannot honestly represent), as is a workflow-backed ("advanced") newsletter. If the newsletter targets a segment whose stored conditions this API cannot evaluate identically to the dashboard, the preview refuses with filter_unsupported rather than publish a number the send worker might disagree with.

The token expires after 15 minutes, and it is invalidated by ANY change to the plan: a subscriber joining or leaving a targeted list, a suppression or unsubscribe landing, content or sending-identity edits, or a different API key confirming. Pass it to POST /newsletters/{newsletterId}/send. Campaign and newsletter confirmation tokens are not interchangeable.

This preview is the dry run for the send flow — the send endpoint itself does not accept dry_run.

Path parameters1
newsletterIdstringrequired1..64 chars
Header parameters1
Idempotency-Keystringrequired≤ 255 chars
A caller-chosen unique string, at most 255 characters. Retrying with the same key replays the first response instead of repeating the work; the replay carries Idempotent-Replayed: true. Reusing a key with a different body is a 409 idempotency_key_reused. Keys are remembered for 24 hours.
curl -X POST "https://api.mailneo.co/api/v2/newsletters/{newsletterId}/send-preview" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
{
  "data": {
    "object": "newsletter_send_preview",
    "newsletter_id": "c11vl0ia125km7911blt4c11l",
    "recipients": {
      "total": 3172,
      "not_opted_in": 1,
      "suppressed": 1,
      "eligible": 1
    },
    "credits_required": 1250,
    "send_cap": {
      "limit": 25,
      "used_today": 1250,
      "remaining": 8750
    },
    "from_address": "hello@example.com",
    "sending_account_address": "hello@example.com",
    "complaint_rate": {
      "rate": 0.0004,
      "threshold": 0.002,
      "window_days": 30
    },
    "confirmation": {
      "token": "ct_0sq7svz0sg87700ta709x0t0",
      "expires_at": "2026-12-31T00:00:00.000Z"
    }
  },
  "meta": {
    "request_id": "a982f00a-aa82-419d-a782-ece4a882ee77"
  }
}
POST/newsletters/{newsletterId}/send

Confirm and start a newsletter send

Requires newsletters:send · Idempotency-Key required

Step 2 of 2. Verifies the confirmation token from send-preview, re-runs every send gate, re-resolves the plan and refuses with 409 send_plan_changed if ANYTHING drifted since the preview (re-preview to proceed), reserves the email credits, and arms the newsletter — the same scheduling flip the app performs. The scheduler starts the send within about a minute, unless the newsletter restricts its send days, in which case it starts on the next allowed day. Arming is irreversible through this API; pausing a running send is an app action.

This endpoint does not accept dry_run — the preview IS the dry run, and unlike a dry run its answer is enforced: the token is bound to the previewed plan, so the send that happens is provably the send that was shown. A token minted by the campaign send flow is never accepted here, nor vice versa.

Idempotency-Key is required; retrying with the same key replays the original response rather than arming twice.

Path parameters1
newsletterIdstringrequired1..64 chars
Header parameters1
Idempotency-Keystringrequired≤ 255 chars
A caller-chosen unique string, at most 255 characters. Retrying with the same key replays the first response instead of repeating the work; the replay carries Idempotent-Replayed: true. Reusing a key with a different body is a 409 idempotency_key_reused. Keys are remembered for 24 hours.
Body parameters1
confirmation_tokenstringrequired8..4096 chars
curl -X POST "https://api.mailneo.co/api/v2/newsletters/{newsletterId}/send" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "confirmation_token": "<confirmation_token from send-preview>"
  }'
{
  "data": {
    "object": "newsletter_send",
    "newsletter_id": "c11vl0ia125km7911blt4c11l",
    "status": "SCHEDULED",
    "send_at": "2026-06-24T15:45:00.000Z",
    "recipients": 1250,
    "credits_reserved": 1250,
    "send_cap": {
      "limit": 25,
      "used_today": 1250,
      "remaining": 8750
    }
  },
  "meta": {
    "request_id": "66cbb45a-67cb-45ed-a4cb-b13465cbb2c7"
  }
}