Campaigns
Campaigns 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: campaigns:read, campaigns:send, campaigns:write. Each endpoint below states the one it requires. Start with the quickstart →
/campaignsList campaigns
Requires campaigns:read
Cursor-paginated. Pass updated_since with the synced_through value from your previous run to fetch only what changed; you will occasionally re-see a row, so upsert by id. Deletes are not visible to updated_since. The message body is omitted here — retrieve a single campaign to get it.
Query parameters7
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" | "SCHEDULED" | "RUNNING" | "PAUSED" | "COMPLETED" | "CANCELLED" | "FAILED"type"COLD" | "NEWSLETTER" | "SEQUENCE"curl "https://api.mailneo.co/api/v2/campaigns?limit=25&order=desc&sort=updated_at" \
-H "x-api-key: $MAILNEO_API_KEY"{
"data": [
{
"object": "campaign",
"id": "c1j7okz31ixoza41jrnsd11jh",
"name": "Spring launch",
"description": "Created via the API.",
"type": "…",
"status": "DRAFT",
"subject": "April product notes",
"preheader": "What shipped this month",
"timezone": "UTC",
"scheduled_at": "2026-06-24T15:45:00.000Z",
"started_at": "2026-05-11T09:30:00.000Z",
"completed_at": "2026-06-24T15:45:00.000Z",
"recipient_count": 1250,
"sent_count": 128,
"delivered_count": 128,
"open_count": 128,
"click_count": 128,
"bounce_count": 128,
"reply_count": 128,
"unsubscribe_count": 128,
"created_at": "2026-05-11T09:30:00.000Z",
"updated_at": "2026-06-24T15:45:00.000Z"
}
],
"meta": {
"request_id": "59cd53a8-5acd-453b-abcd-56ce5ccd5861",
"has_more": false,
"next_cursor": null,
"synced_through": "2026-06-24T15:45:00.000Z"
}
}/campaignsCreate a campaign draft
Requires campaigns:write · Idempotency-Key optional
Creates one campaign, always as a DRAFT — this endpoint cannot send, schedule or arm anything. status, scheduled_at and the v1 spelling scheduledAt are refused by name with a 400 (never silently ignored): arming a send is campaigns:send, a separate scope with its own two-step confirmation flow (POST /campaigns/{campaignId}/send-preview, then .../send).
email_account_ids must be ids of ACTIVE sending accounts in your team; an unknown, inactive or foreign id fails the whole write with a 404 naming it. They are stored but never echoed back — reading sending-account identity is accounts:read.
Every campaign created here is type SEQUENCE (the app creates no other kind), and names are not required to be unique — two campaigns 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 parameters10
namestringrequired1..200 charsdescriptionstring · nullable≤ 1000 charssubjectstring · nullable≤ 5000 charspreheaderstring · nullable≤ 5000 charsbodystring · nullable≤ 524288 charstimezonestring · nullable≤ 100 charsemail_account_idsstring[]≤ 50 itemsstatusanyscheduled_atanyscheduledAtanycurl -X POST "https://api.mailneo.co/api/v2/campaigns" \
-H "x-api-key: $MAILNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Lovelace",
"description": "Created via the API.",
"subject": "April product notes",
"preheader": "What shipped this month",
"body": "<p>Hi {{first_name}},</p>",
"timezone": "UTC",
"email_account_ids": [
"c13jh9k1139hnv212zi26312p"
],
"status": "DRAFT"
}'{
"data": "…",
"meta": {
"request_id": "c6a4d7b5-c5a4-4622-a4a4-d48fc3a4d2fc",
"dry_run": false
}
}/campaigns/{campaignId}Retrieve a campaign
Requires campaigns:read
Returns the full campaign including the message body.
Path parameters1
campaignIdstringrequired1..64 charscurl "https://api.mailneo.co/api/v2/campaigns/{campaignId}" \
-H "x-api-key: $MAILNEO_API_KEY"{
"data": {
"object": "campaign",
"id": "c1j7okz31ixoza41jrnsd11jh",
"name": "Spring launch",
"description": "Created via the API.",
"type": "…",
"status": "DRAFT",
"subject": "April product notes",
"preheader": "What shipped this month",
"timezone": "UTC",
"scheduled_at": "2026-06-24T15:45:00.000Z",
"started_at": "2026-05-11T09:30:00.000Z",
"completed_at": "2026-06-24T15:45:00.000Z",
"recipient_count": 1250,
"sent_count": 128,
"delivered_count": 128,
"open_count": 128,
"click_count": 128,
"bounce_count": 128,
"reply_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>"
},
"meta": {
"request_id": "b7d247ed-b6d2-465a-a5d2-44c7b4d24334"
}
}/campaigns/{campaignId}Update a campaign draft
Requires campaigns:write · Idempotency-Key optional
Merge-patch: fields you omit are left alone, and null clears description, subject, preheader or body (timezone resets to "UTC"). Only a DRAFT campaign can be edited — anything else is a 409 resource_state_invalid: a SCHEDULED campaign is armed and editing it would retime or reshape a pending send, RUNNING/PAUSED are mid-send, and COMPLETED/CANCELLED/FAILED are the record of what was actually sent. Un-schedule or stop it in the app first.
status, scheduled_at and scheduledAt are refused by name with a 400 — arming a send is campaigns:send, through its two-step confirmation flow (POST /campaigns/{campaignId}/send-preview, then .../send).
Path parameters1
campaignIdstringrequired1..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 parameters10
namestring1..200 charsdescriptionstring · nullable≤ 1000 charssubjectstring · nullable≤ 5000 charspreheaderstring · nullable≤ 5000 charsbodystring · nullable≤ 524288 charstimezonestring · nullable≤ 100 charsemail_account_idsstring[]≤ 50 itemsstatusanyscheduled_atanyscheduledAtanycurl -X PATCH "https://api.mailneo.co/api/v2/campaigns/{campaignId}" \
-H "x-api-key: $MAILNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Lovelace",
"description": "Created via the API.",
"subject": "April product notes",
"preheader": "What shipped this month",
"body": "<p>Hi {{first_name}},</p>",
"timezone": "UTC",
"email_account_ids": [
"c13jh9k1139hnv212zi26312p"
],
"status": "DRAFT"
}'{
"data": "…",
"meta": {
"request_id": "749d854c-759d-46df-a69d-8872779d8a05",
"dry_run": false
}
}/campaigns/{campaignId}Delete a campaign draft
Requires campaigns: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 send history: the per-recipient delivery records and every open/click/bounce event row go with the campaign, and the individual sent messages are detached from it. For a campaign 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 campaign row: its sequence steps and any not-yet-sent recipient rows.
Path parameters1
campaignIdstringrequired1..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/campaigns/{campaignId}" \
-H "x-api-key: $MAILNEO_API_KEY"{
"data": {
"object": "campaign",
"id": "c1j7okz31ixoza41jrnsd11jh",
"deleted": true
},
"meta": {
"request_id": "4a1e810e-4b1e-42a1-a81e-7de8491e7f7b",
"dry_run": false
}
}/campaigns/{campaignId}/send-previewPreview a campaign send and get a confirmation token
Requires campaigns:send · Idempotency-Key required
Step 1 of 2. Resolves exactly what a send would do — recipients remaining after suppression and validity filtering, credits required, sending addresses, 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.
The token expires after 15 minutes, and it is invalidated by ANY change to the plan: a recipient added or removed, a suppression landing, content or sending-account edits, or a different API key confirming. Pass it to POST /campaigns/{campaignId}/send.
This preview is the dry run for the send flow — the send endpoint itself does not accept dry_run.
Path parameters1
campaignIdstringrequired1..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/campaigns/{campaignId}/send-preview" \
-H "x-api-key: $MAILNEO_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"{
"data": {
"object": "campaign_send_preview",
"campaign_id": "c1j7okz31ixoza41jrnsd11jh",
"recipients": {
"total": 3172,
"invalid": 1,
"suppressed": 1,
"eligible": 1
},
"credits_required": 1250,
"send_cap": {
"limit": 25,
"used_today": 1250,
"remaining": 8750
},
"from_addresses": [
"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": "e2d880ef-e1d8-4f5c-a4d8-8415e3d88282"
}
}/campaigns/{campaignId}/sendConfirm and start a campaign send
Requires campaigns: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 campaign — the same scheduling flip the app performs. The scheduler starts the send within about a minute. 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.
Idempotency-Key is required; retrying with the same key replays the original response rather than arming twice.
Path parameters1
campaignIdstringrequired1..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/campaigns/{campaignId}/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": "campaign_send",
"campaign_id": "c1j7okz31ixoza41jrnsd11jh",
"status": "SCHEDULED",
"scheduled_at": "2026-06-24T15:45:00.000Z",
"recipients": 1250,
"credits_reserved": 1250,
"send_cap": {
"limit": 25,
"used_today": 1250,
"remaining": 8750
}
},
"meta": {
"request_id": "ddd95847-dcd9-46b4-afd9-5b6dded959da"
}
}