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 →
/listsList 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..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_attype"PUBLIC" | "PRIVATE" | "TEMPORARY"optin"SINGLE" | "DOUBLE"namestring≤ 200 charscurl "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"
}
}/listsCreate 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_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 parameters5
namestringrequired1..200 charsdescriptionstring≤ 500 charstype"PUBLIC" | "PRIVATE" | "TEMPORARY"optin"SINGLE" | "DOUBLE"tagsstring[]≤ 20 itemscurl -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
}
}/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 charscurl "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"
}
}/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 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 parameters5
namestring1..200 charsdescriptionstring≤ 500 charstype"PUBLIC" | "PRIVATE" | "TEMPORARY"optin"SINGLE" | "DOUBLE"tagsstring[]≤ 20 itemscurl -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
}
}/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 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/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
}
}/lists/{listId}/subscribersList 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 charsQuery 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"ENABLED" | "DISABLED" | "BLOCKLISTED"emailstring≤ 320 charssourcestring≤ 200 charstagsstring≤ 1000 charscurl "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"
}
}/lists/{listId}/subscribersAdd 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 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 parameters1
subscriber_idsstring[]required1..100 itemscurl -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
}
}/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 charssubscriberIdstringrequired1..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/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
}
}