Newsletter

Subscribers API

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

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

GET/subscribers

List newsletter subscribers

Requires subscribers: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.

Subscribers are distinct from contacts: a contact is a cold-outreach record, a subscriber is on a newsletter list. Creating one does not create the other.

Query parameters10
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
list_idstring≤ 64 chars
curl "https://api.mailneo.co/api/v2/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": "633dbfb4-643d-4147-a53d-c2da663dc46d",
    "has_more": false,
    "next_cursor": null,
    "synced_through": "2026-06-24T15:45:00.000Z"
  }
}
POST/subscribers

Create a subscriber

Requires subscribers:write · Idempotency-Key optional

Creates one subscriber. The email is normalised (trimmed and lower-cased) and must be unique within the team — a duplicate is a 409 resource_already_exists, not a silent upsert, unlike the v1 endpoint this replaces. When name is omitted it is composed from first_name and last_name, matching what the app does.

status accepts ENABLED or DISABLED. BLOCKLISTED cannot be written in either direction: it is the stored record of a global opt-out, owned by the unsubscribe and suppression machinery.

The new subscriber is on NO list — attach them with POST /lists/{listId}/subscribers, which requires lists:write and lets the list's own opt-in setting decide how the subscription starts. Subscribers are distinct from contacts: creating one does not create the other.

Automations do not run on subscribers created this way: a "subscriber is created" trigger will not fire, and no webhook is emitted.

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
emailstringrequired3..320 chars
namestring≤ 200 chars
first_namestring · nullable≤ 200 chars
last_namestring · nullable≤ 200 chars
phonestring · nullable≤ 50 chars
companystring · nullable≤ 200 chars
sourcestring · nullable≤ 200 chars
status"ENABLED" | "DISABLED"
tagsstring[]≤ 50 items
custom_fieldsobject
curl -X POST "https://api.mailneo.co/api/v2/subscribers" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ada@example.com",
    "name": "Ada Lovelace",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "phone": "+1 555 0142",
    "company": "Acme Inc",
    "source": "api",
    "status": "ENABLED"
  }'
{
  "data": "…",
  "meta": {
    "request_id": "45f78d41-44f7-4bae-a3f7-8a1b42f78888",
    "dry_run": false
  }
}
GET/subscribers/{subscriberId}

Retrieve a subscriber

Requires subscribers:read

Path parameters1
subscriberIdstringrequired1..64 chars
curl "https://api.mailneo.co/api/v2/subscribers/{subscriberId}" \
  -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": "f6e83739-f5e8-45a6-a4e8-3413f3e83280"
  }
}
PATCH/subscribers/{subscriberId}

Update a subscriber

Requires subscribers:write · Idempotency-Key optional

Merge-patch: fields you omit are left alone, and null clears a nullable field (name is not nullable — clear it with ""). tags REPLACES the whole array — v1 merged them, this does not — while custom_fields merges key by key so a value written by an import is not lost; clear one custom field by sending it as null.

Changing email is allowed and must not collide with another subscriber in the team (409 resource_already_exists).

A BLOCKLISTED subscriber — someone who globally opted out — keeps that status: sending status for them is a 409 resource_state_invalid, because re-enabling an opt-out is not a routine write. Their other fields can still be edited.

List membership is not editable here — that is POST /lists/{listId}/subscribers under lists:write. Field-update automations do not run on edits made this way.

Path parameters1
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.
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
emailstring3..320 chars
namestring≤ 200 chars
first_namestring · nullable≤ 200 chars
last_namestring · nullable≤ 200 chars
phonestring · nullable≤ 50 chars
companystring · nullable≤ 200 chars
sourcestring · nullable≤ 200 chars
status"ENABLED" | "DISABLED"
tagsstring[]≤ 50 items
custom_fieldsobject
curl -X PATCH "https://api.mailneo.co/api/v2/subscribers/{subscriberId}" \
  -H "x-api-key: $MAILNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ada@example.com",
    "name": "Ada Lovelace",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "phone": "+1 555 0142",
    "company": "Acme Inc",
    "source": "api",
    "status": "ENABLED"
  }'
{
  "data": "…",
  "meta": {
    "request_id": "c53e40c4-c63e-4257-a73e-43eac83e457d",
    "dry_run": false
  }
}
DELETE/subscribers/{subscriberId}

Delete a subscriber

Requires subscribers:delete

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

The subscriber row is removed and their engagement trail goes with it: list memberships cascade — INCLUDING any per-list unsubscribe records, so re-creating the same address and re-adding it to a list would mail someone who had left that list — and open/click tracking rows are deleted. The send log is kept for campaign analytics with its email anonymised in the same transaction.

A GLOBAL opt-out survives: the suppression list is keyed by email address, not by subscriber, and deleting the subscriber does not touch it. This is also not a complete GDPR erasure for the person — the address may still appear as a contact or in the inbox; use the account data-erasure flow for that.

Path parameters1
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/subscribers/{subscriberId}" \
  -H "x-api-key: $MAILNEO_API_KEY"
{
  "data": {
    "object": "subscriber",
    "id": "c0oevquz0o4w5600oyuy8x0oo",
    "deleted": true
  },
  "meta": {
    "request_id": "2314bc1e-2414-4db1-a114-b8f82214ba8b",
    "dry_run": false
  }
}