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 →
/newslettersList 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..100cursorstring≤ 256 charsnext_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: descupdated_sincestring · date-timesort"updated_at" | "created_at"default: updated_atstatus"DRAFT" | "SENDING" | "SCHEDULED" | "PAUSED" | "FAILED" | "SENT" | "ACTIVE" | "CANCELLED"type"REGULAR" | "OPTIN"campaign_mode"broadcast" | "drip"namestring≤ 200 charscurl "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"
}
}/newslettersCreate 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_runbooleandata: 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 charsIdempotent-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 charssubjectstringrequired1..5000 charsfrom_emailstringrequired3..320 charsbodystringrequired1..524288 charspreheaderstring · nullable≤ 5000 charsfrom_namestring · nullable≤ 200 charsreply_tostring · nullable3..320 charsalt_bodystring · nullable≤ 524288 charscontent_type"RICHTEXT" | "HTML" | "PLAIN" | "VISUAL"tagsstring[]≤ 50 itemsemail_account_idstring · nullable1..64 charsstatusanysend_atanysendAtanysend_daysanysendDaysanyis_dripanyisDripanycampaign_modeanycampaignModeanydrip_auto_enrollanydripAutoEnrollanycurl -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
}
}/newsletters/{newsletterId}Retrieve a newsletter
Requires newsletters:read
Includes body and alt_body, which the list response omits.
Path parameters1
newsletterIdstringrequired1..64 charscurl "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"
}
}/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 charsQuery parameters1
dry_runbooleandata: 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 charsIdempotent-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 charssubjectstring1..5000 charsfrom_emailstring3..320 charsbodystring1..524288 charspreheaderstring · nullable≤ 5000 charsfrom_namestring · nullable≤ 200 charsreply_tostring · nullable3..320 charsalt_bodystring · nullable≤ 524288 charscontent_type"RICHTEXT" | "HTML" | "PLAIN" | "VISUAL"tagsstring[]≤ 50 itemsemail_account_idstring · nullable1..64 charsstatusanysend_atanysendAtanysend_daysanysendDaysanyis_dripanyisDripanycampaign_modeanycampaignModeanydrip_auto_enrollanydripAutoEnrollanycurl -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
}
}/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 charsQuery parameters1
dry_runbooleandata: 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
}
}/newsletters/{newsletterId}/send-previewPreview 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 charsHeader parameters1
Idempotency-Keystringrequired≤ 255 charsIdempotent-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"
}
}/newsletters/{newsletterId}/sendConfirm 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 charsHeader parameters1
Idempotency-Keystringrequired≤ 255 charsIdempotent-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 charscurl -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"
}
}