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 →

GET/campaigns

List 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..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" | "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"
  }
}
POST/campaigns

Create 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_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 parameters10
namestringrequired1..200 chars
descriptionstring · nullable≤ 1000 chars
subjectstring · nullable≤ 5000 chars
preheaderstring · nullable≤ 5000 chars
bodystring · nullable≤ 524288 chars
timezonestring · nullable≤ 100 chars
email_account_idsstring[]≤ 50 items
statusany
scheduled_atany
scheduledAtany
curl -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
  }
}
GET/campaigns/{campaignId}

Retrieve a campaign

Requires campaigns:read

Returns the full campaign including the message body.

Path parameters1
campaignIdstringrequired1..64 chars
curl "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"
  }
}
PATCH/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 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 parameters10
namestring1..200 chars
descriptionstring · nullable≤ 1000 chars
subjectstring · nullable≤ 5000 chars
preheaderstring · nullable≤ 5000 chars
bodystring · nullable≤ 524288 chars
timezonestring · nullable≤ 100 chars
email_account_idsstring[]≤ 50 items
statusany
scheduled_atany
scheduledAtany
curl -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
  }
}
DELETE/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 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/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
  }
}
POST/campaigns/{campaignId}/send-preview

Preview 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 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/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"
  }
}
POST/campaigns/{campaignId}/send

Confirm 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 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/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"
  }
}