Newsletter

Lists API

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

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

GET/lists

List subscriber lists

Requires lists:read

Cursor-paginated. Pass updated_since with the synced_through value from your previous run to fetch only what changed.

subscriber_count is not on this response — it costs one COUNT(*) per list and would be 100 of them on a full page. Retrieve a single list to get it, or page GET /lists/{listId}/subscribers.

Query parameters8
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
type"PUBLIC" | "PRIVATE" | "TEMPORARY"
optin"SINGLE" | "DOUBLE"
namestring≤ 200 chars
curl "https://api.mailneo.co/api/v2/lists?limit=25&order=desc&sort=updated_at" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": [
    {
      "object": "list",
      "id": "c0d6j7zl0cwjmam0cmk0ln0cc",
      "name": "Product updates",
      "description": "Created via the API.",
      "type": "…",
      "optin": "…",
      "tags": [
        "vip"
      ],
      "created_at": "2026-05-11T09:30:00.000Z",
      "updated_at": "2026-06-24T15:45:00.000Z"
    }
  ],
  "meta": {
    "request_id": "406f01a2-416f-4335-ae6e-fe7c3f6f000f",
    "has_more": false,
    "next_cursor": null,
    "synced_through": "2026-06-24T15:45:00.000Z"
  }
}
POST/lists

Create a list

Requires lists:write · Idempotency-Key optional

Creates one empty list. Names are NOT required to be unique — the app does not enforce it either, so two lists may share a name and are told apart by id.

optin decides how a subscription to this list starts: on a SINGLE list a new member is CONFIRMED immediately, on a DOUBLE list they are UNCONFIRMED until they click a confirmation link. Choose it now — changing it later does not revisit the members already on the list.

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 parameters5
namestringrequired1..200 chars
descriptionstring≤ 500 chars
type"PUBLIC" | "PRIVATE" | "TEMPORARY"
optin"SINGLE" | "DOUBLE"
tagsstring[]≤ 20 items
curl -X POST "https://api.mailneo.co/api/v2/lists" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "description": "Created via the API.",
    "type": "PUBLIC",
    "optin": "SINGLE",
    "tags": [
      "vip"
    ]
  }'
{
  "data": "…",
  "meta": {
    "request_id": "9baba2cf-9aab-413c-adab-a5f59caba462",
    "dry_run": false
  }
}
GET/lists/{listId}

Retrieve a list

Requires lists:read

Includes subscriber_count: every subscriber who is a member of this list, whatever their status and whatever their per-list subscription state.

Path parameters1
listIdstringrequired1..64 chars
curl "https://api.mailneo.co/api/v2/lists/{listId}" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": {
    "object": "list",
    "id": "c0d6j7zl0cwjmam0cmk0ln0cc",
    "name": "Product updates",
    "description": "Created via the API.",
    "type": "…",
    "optin": "…",
    "tags": [
      "vip"
    ],
    "created_at": "2026-05-11T09:30:00.000Z",
    "updated_at": "2026-06-24T15:45:00.000Z",
    "subscriber_count": 3172
  },
  "meta": {
    "request_id": "0fcb7877-0ecb-46e4-a1cb-7b9d10cb7a0a"
  }
}
PATCH/lists/{listId}

Update a list

Requires lists:write · Idempotency-Key optional

Merge-patch: fields you omit are left alone. Nothing on a list is nullable, so sending null for a field is a 400 — clear description with "" and tags with [].

Changing optin applies to FUTURE members only. Moving a list from SINGLE to DOUBLE does not retroactively unconfirm anyone already on it, and moving it the other way does not confirm anyone.

Path parameters1
listIdstringrequired1..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 parameters5
namestring1..200 chars
descriptionstring≤ 500 chars
type"PUBLIC" | "PRIVATE" | "TEMPORARY"
optin"SINGLE" | "DOUBLE"
tagsstring[]≤ 20 items
curl -X PATCH "https://api.mailneo.co/api/v2/lists/{listId}" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "description": "Created via the API.",
    "type": "PUBLIC",
    "optin": "SINGLE",
    "tags": [
      "vip"
    ]
  }'
{
  "data": "…",
  "meta": {
    "request_id": "427b4726-437b-48b9-a07b-4400417b4593",
    "dry_run": false
  }
}
DELETE/lists/{listId}

Delete a list

Requires lists:write

Permanent, and there is no undo — call with ?dry_run=true first.

The subscribers on the list are NOT deleted; they keep every other list they are on. What is removed is their membership of THIS list, including any record that someone had unsubscribed from it — so re-creating the list and re-adding the same people will mail addresses that had opted out of it. Prefer leaving an unused list in place.

Two references are silently detached rather than blocked. A newsletter campaign that targeted this list keeps its targeting row with the list unset (the list NAME is retained on it for reporting), and any integration configured to sync into this list is left with no destination and will stop delivering into it. Check both before deleting a list something else feeds.

Path parameters1
listIdstringrequired1..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/lists/{listId}" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": {
    "object": "list",
    "id": "c0d6j7zl0cwjmam0cmk0ln0cc",
    "deleted": true
  },
  "meta": {
    "request_id": "8c6e7078-8d6e-420b-ae6e-739e8f6e7531",
    "dry_run": false
  }
}
GET/lists/{listId}/subscribers

List the subscribers on a list

Requires subscribers:read

The members of one list, in the same subscriber shape and with the same filters as GET /subscribers. Cursor-paginated, and updated_since tracks the SUBSCRIBER's updated_at, not the membership row's — so a subscriber added to this list without being edited will not appear in an incremental poll. Do a full walk when you need membership changes.

Requires subscribers:read, not lists:read: this returns people, not lists. It is exactly equivalent to GET /subscribers?list_id=, which is gated the same way.

The membership record itself is never returned. It holds a double-opt-in confirmation token, which is a bearer secret.

Path parameters1
listIdstringrequired1..64 chars
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"ENABLED" | "DISABLED" | "BLOCKLISTED"
emailstring≤ 320 chars
sourcestring≤ 200 chars
tagsstring≤ 1000 chars
curl "https://api.mailneo.co/api/v2/lists/{listId}/subscribers?limit=25&order=desc&sort=updated_at" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": [
    {
      "object": "subscriber",
      "id": "c0oevquz0o4w5600oyuy8x0oo",
      "email": "ada@example.com",
      "name": "Ada Lovelace",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "phone": "+1 555 0142",
      "company": "Acme Inc",
      "status": "ENABLED",
      "tags": [
        "vip"
      ],
      "source": "api",
      "custom_fields": {
        "plan": "pro"
      },
      "last_contacted_at": "2026-06-24T15:45:00.000Z",
      "created_at": "2026-05-11T09:30:00.000Z",
      "updated_at": "2026-06-24T15:45:00.000Z"
    }
  ],
  "meta": {
    "request_id": "efcf41dc-f0cf-436f-a1cf-4502f2cf4695",
    "has_more": false,
    "next_cursor": null,
    "synced_through": "2026-06-24T15:45:00.000Z"
  }
}
POST/lists/{listId}/subscribers

Add subscribers to a list

Requires lists:write · Idempotency-Key optional

Attaches subscribers that already exist in your team to this list, up to 100 per call. It never CREATES a subscriber — that is subscribers:write — so an id with no subscriber behind it comes back under not_found rather than being minted.

Partial success is the normal outcome and the response says exactly which ids landed where: added, already_member (left alone, so re-posting the same batch is safe), unsubscribed and not_found.

Someone who previously unsubscribed from this list is NOT re-added. They are reported under unsubscribed and their opt-out stands; re-subscribing an opt-out is a deliberate act and this endpoint is the routine one.

New members start CONFIRMED on a SINGLE opt-in list and UNCONFIRMED on a DOUBLE one, decided by the list — there is no request field that overrides it. Note that on a DOUBLE list this endpoint sends no confirmation email, so those members stay unmailable until they confirm through one of your own flows.

Automations do not run on membership added this way: a "subscriber joins list" trigger will not fire for these people.

Path parameters1
listIdstringrequired1..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 parameters1
subscriber_idsstring[]required1..100 items
curl -X POST "https://api.mailneo.co/api/v2/lists/{listId}/subscribers" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriber_ids": [
      "c0oevquz0o4w5600oyuy8x0oo"
    ]
  }'
{
  "data": {
    "object": "list_membership",
    "list_id": "c0d6j7zl0cwjmam0cmk0ln0cc",
    "added": [
      "…"
    ],
    "already_member": [
      "…"
    ],
    "unsubscribed": [
      "…"
    ],
    "not_found": [
      "…"
    ]
  },
  "meta": {
    "request_id": "7ae0aff1-79e0-4e5e-a8e0-accb77e0ab38",
    "dry_run": false
  }
}
DELETE/lists/{listId}/subscribers/{subscriberId}

Unsubscribe a subscriber from a list

Requires lists:write

Removes one subscriber from this list's mailable membership by marking their subscription UNSUBSCRIBED.

The membership record is deliberately KEPT rather than deleted, which is why this answers with the membership rather than deleted: true. The record IS the opt-out: deleting it would leave nothing to say this person had ever left, and the next POST /lists/{listId}/subscribers would silently re-add them and start mailing them again. For the same reason this is not reversible through the API — re-adding them reports unsubscribed and changes nothing.

The subscriber itself is untouched and keeps every other list they are on. They still count towards this list's subscriber_count, which counts membership rather than mailability.

Calling this again is a no-op that succeeds. A subscriber who was never on the list is a 404.

Path parameters2
listIdstringrequired1..64 chars
subscriberIdstringrequired1..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/lists/{listId}/subscribers/{subscriberId}" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": {
    "object": "list_membership",
    "list_id": "c0d6j7zl0cwjmam0cmk0ln0cc",
    "subscriber_id": "c0oevquz0o4w5600oyuy8x0oo",
    "status": "UNSUBSCRIBED"
  },
  "meta": {
    "request_id": "6d2c885d-6c2c-46ca-ab2c-85376a2c83a4",
    "dry_run": false
  }
}