Core
Subscribers API
5 endpoints under https://api.mailneo.co/api/v2. Every request carries your key in the X-API-Key header. The scopes used here: subscribers:delete, subscribers:read, subscribers:write. Each endpoint below states the one it requires. Start with the guide →
listSubscribersList newsletter subscribers
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.
/subscriberssubscribers:readExample request
curl "https://api.mailneo.co/api/v2/subscribers?limit=25&order=desc&sort=updated_at" \ -H "X-API-Key: $MAILNEO_API_KEY"
Authorization
- Required scope
subscribers:read. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...
Query parameters10
| Name | Type | Required | Notes |
|---|---|---|---|
| limit | integer | No | default 25 · 1–100 |
| cursor | string | No | max 256 chars |
| order | "asc" | "desc" | No | default "desc" |
| updated_since | string · date-time | No | ISO 8601 |
| sort | "updated_at" | "created_at" | No | default "updated_at" |
| status | "ENABLED" | "DISABLED" | "BLOCKLISTED" | No | |
| string | No | max 320 chars | |
| source | string | No | max 200 chars |
| tags | string | No | max 1000 chars |
| list_id | string | No | max 64 chars |
Response · 200
Success
data[]Each element of the array.
| Field | Type | Always present | Notes |
|---|---|---|---|
| object | "subscriber" | Yes | |
| id | string | Yes | |
| string | Yes | ||
| name | string | Yes | |
| first_name | string · nullable | Yes | |
| last_name | string · nullable | Yes | |
| phone | string · nullable | Yes | |
| company | string · nullable | Yes | |
| status | string | Yes | |
| tags | string[] | Yes | |
| source | string · nullable | Yes | |
| custom_fields | object | Yes | open keys |
| last_contacted_at | string · nullable | Yes | |
| created_at | string | Yes | |
| updated_at | string | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes | |
| has_more | boolean | Yes | |
| next_cursor | string · nullable | Yes | |
| synced_through | string | No |
createSubscriberCreate a subscriber
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.
/subscriberssubscribers:writeExample request
curl -X POST "https://api.mailneo.co/api/v2/subscribers" \
-H "X-API-Key: $MAILNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "<string>"
}'Authorization
- Required scope
subscribers:write. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...- Idempotency
Idempotency-Keyis optional.
Query parameters1
| Name | Type | Required | Notes |
|---|---|---|---|
| dry_run | boolean | No | 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
| Name | Type | Required | Notes |
|---|---|---|---|
| Idempotency-Key | string | No | 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. · max 255 chars |
Request body
Body
| Field | Type | Always present | Notes |
|---|---|---|---|
| string | Yes | max 320 chars · pattern-constrained | |
| name | string | No | max 200 chars |
| first_name | string · nullable | No | max 200 chars |
| last_name | string · nullable | No | max 200 chars |
| phone | string · nullable | No | max 50 chars |
| company | string · nullable | No | max 200 chars |
| source | string · nullable | No | max 200 chars |
| status | "ENABLED" | "DISABLED" | No | |
| tags | string[] | No | |
| custom_fields | object | No | open keys |
Response · 200
Success
data
| Field | Type | Always present | Notes |
|---|---|---|---|
| data | object | object · nullable | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes | |
| dry_run | boolean | Yes | |
| impact | object | No | |
| impact.object | "impact" | Yes | |
| impact.would_create | integer | Yes | |
| impact.would_update | integer | Yes | |
| impact.would_delete | integer | Yes | |
| impact.affected_count | integer | Yes | |
| impact.affected_ids | string[] | Yes | |
| impact.quota | object · nullable | Yes | |
| impact.quota.kind | string | Yes | |
| impact.quota.would_consume | integer | Yes | |
| impact.quota.limit | integer · nullable | Yes | |
| impact.quota.current | integer | Yes |
Response · 201
Created
data
| Field | Type | Always present | Notes |
|---|---|---|---|
| data | object | object · nullable | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes | |
| dry_run | boolean | Yes | |
| impact | object | No | |
| impact.object | "impact" | Yes | |
| impact.would_create | integer | Yes | |
| impact.would_update | integer | Yes | |
| impact.would_delete | integer | Yes | |
| impact.affected_count | integer | Yes | |
| impact.affected_ids | string[] | Yes | |
| impact.quota | object · nullable | Yes | |
| impact.quota.kind | string | Yes | |
| impact.quota.would_consume | integer | Yes | |
| impact.quota.limit | integer · nullable | Yes | |
| impact.quota.current | integer | Yes |
getSubscriberRetrieve a subscriber
/subscribers/{subscriberId}subscribers:readExample request
curl "https://api.mailneo.co/api/v2/subscribers/{subscriberId}" \
-H "X-API-Key: $MAILNEO_API_KEY"Authorization
- Required scope
subscribers:read. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...
Path parameters1
| Name | Type | Required | Notes |
|---|---|---|---|
| subscriberId | string | Yes | max 64 chars |
Response · 200
Success
data
| Field | Type | Always present | Notes |
|---|---|---|---|
| object | "subscriber" | Yes | |
| id | string | Yes | |
| string | Yes | ||
| name | string | Yes | |
| first_name | string · nullable | Yes | |
| last_name | string · nullable | Yes | |
| phone | string · nullable | Yes | |
| company | string · nullable | Yes | |
| status | string | Yes | |
| tags | string[] | Yes | |
| source | string · nullable | Yes | |
| custom_fields | object | Yes | open keys |
| last_contacted_at | string · nullable | Yes | |
| created_at | string | Yes | |
| updated_at | string | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes |
updateSubscriberUpdate a subscriber
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.
/subscribers/{subscriberId}subscribers:writeExample request
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": "<string>",
"name": "<string>",
"first_name": "<string>",
"last_name": "<string>",
"phone": "<string>",
"company": "<string>",
"source": "<string>",
"status": "ENABLED",
"tags": [
"<string>"
],
"custom_fields": {}
}'Authorization
- Required scope
subscribers:write. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...- Idempotency
Idempotency-Keyis optional.
Path parameters1
| Name | Type | Required | Notes |
|---|---|---|---|
| subscriberId | string | Yes | max 64 chars |
Query parameters1
| Name | Type | Required | Notes |
|---|---|---|---|
| dry_run | boolean | No | 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
| Name | Type | Required | Notes |
|---|---|---|---|
| Idempotency-Key | string | No | 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. · max 255 chars |
Request body
Body
| Field | Type | Always present | Notes |
|---|---|---|---|
| string | No | max 320 chars · pattern-constrained | |
| name | string | No | max 200 chars |
| first_name | string · nullable | No | max 200 chars |
| last_name | string · nullable | No | max 200 chars |
| phone | string · nullable | No | max 50 chars |
| company | string · nullable | No | max 200 chars |
| source | string · nullable | No | max 200 chars |
| status | "ENABLED" | "DISABLED" | No | |
| tags | string[] | No | |
| custom_fields | object | No | open keys |
Response · 200
Success
data
| Field | Type | Always present | Notes |
|---|---|---|---|
| data | object | object · nullable | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes | |
| dry_run | boolean | Yes | |
| impact | object | No | |
| impact.object | "impact" | Yes | |
| impact.would_create | integer | Yes | |
| impact.would_update | integer | Yes | |
| impact.would_delete | integer | Yes | |
| impact.affected_count | integer | Yes | |
| impact.affected_ids | string[] | Yes | |
| impact.quota | object · nullable | Yes | |
| impact.quota.kind | string | Yes | |
| impact.quota.would_consume | integer | Yes | |
| impact.quota.limit | integer · nullable | Yes | |
| impact.quota.current | integer | Yes |
deleteSubscriberDelete a subscriber
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.
/subscribers/{subscriberId}subscribers:deleteExample request
curl -X DELETE "https://api.mailneo.co/api/v2/subscribers/{subscriberId}" \
-H "X-API-Key: $MAILNEO_API_KEY"Authorization
- Required scope
subscribers:delete. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...
Path parameters1
| Name | Type | Required | Notes |
|---|---|---|---|
| subscriberId | string | Yes | max 64 chars |
Query parameters1
| Name | Type | Required | Notes |
|---|---|---|---|
| dry_run | boolean | No | 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. |
Response · 200
Success
dataMay be null.
| Field | Type | Always present | Notes |
|---|---|---|---|
| object | "subscriber" | Yes | |
| id | string | Yes | |
| deleted | true | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes | |
| dry_run | boolean | Yes | |
| impact | object | No | |
| impact.object | "impact" | Yes | |
| impact.would_create | integer | Yes | |
| impact.would_update | integer | Yes | |
| impact.would_delete | integer | Yes | |
| impact.affected_count | integer | Yes | |
| impact.affected_ids | string[] | Yes | |
| impact.quota | object · nullable | Yes | |
| impact.quota.kind | string | Yes | |
| impact.quota.would_consume | integer | Yes | |
| impact.quota.limit | integer · nullable | Yes | |
| impact.quota.current | integer | Yes |