Core
Contacts API
6 endpoints under https://api.mailneo.co/api/v2. Every request carries your key in the X-API-Key header. The scopes used here: contacts:delete, contacts:read, contacts:write. Each endpoint below states the one it requires. Start with the guide →
listContactsList contacts
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.
/contactscontacts:readExample request
curl "https://api.mailneo.co/api/v2/contacts?limit=25&order=desc&sort=updated_at" \ -H "X-API-Key: $MAILNEO_API_KEY"
Authorization
- Required scope
contacts:read. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...
Query parameters9
| 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" | "last_activity_at" | No | default "updated_at" |
| status | "ACTIVE" | "CONVERTED" | "LOST" | "UNQUALIFIED" | "ARCHIVED" | No | |
| string | No | max 320 chars | |
| source | string | No | max 200 chars |
| tags | string | No | max 1000 chars |
Response · 200
Success
data[]Each element of the array.
| Field | Type | Always present | Notes |
|---|---|---|---|
| object | "contact" | Yes | |
| id | string | Yes | |
| string | Yes | ||
| first_name | string · nullable | Yes | |
| last_name | string · nullable | Yes | |
| company | string · nullable | Yes | |
| phone | string · nullable | Yes | |
| website | string · nullable | Yes | |
| string · nullable | Yes | ||
| string · nullable | Yes | ||
| string · nullable | Yes | ||
| status | string | Yes | |
| tags | string[] | Yes | |
| source | string · nullable | Yes | |
| custom_fields | object | Yes | open keys |
| emails_opened_count | number | Yes | |
| links_clicked_count | number | Yes | |
| total_interactions | number | Yes | |
| has_replied | boolean | Yes | |
| last_contacted_at | string · nullable | Yes | |
| last_activity_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 |
createContactCreate a contact
Creates one contact. 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. Use POST /contacts/bulk when you want upsert semantics.
/contactscontacts:writeExample request
curl -X POST "https://api.mailneo.co/api/v2/contacts" \
-H "X-API-Key: $MAILNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "<string>"
}'Authorization
- Required scope
contacts: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 | |
| first_name | string · nullable | No | max 200 chars |
| last_name | string · nullable | No | max 200 chars |
| company | string · nullable | No | max 200 chars |
| phone | string · nullable | No | max 50 chars |
| website | string · nullable | No | max 500 chars |
| string · nullable | No | max 500 chars | |
| string · nullable | No | max 500 chars | |
| string · nullable | No | max 500 chars | |
| source | string · nullable | No | max 200 chars |
| status | "ACTIVE" | "CONVERTED" | "LOST" | "UNQUALIFIED" | "ARCHIVED" | 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 |
getContactRetrieve a contact
/contacts/{contactId}contacts:readExample request
curl "https://api.mailneo.co/api/v2/contacts/{contactId}" \
-H "X-API-Key: $MAILNEO_API_KEY"Authorization
- Required scope
contacts: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 |
|---|---|---|---|
| contactId | string | Yes | max 64 chars |
Response · 200
Success
data
| Field | Type | Always present | Notes |
|---|---|---|---|
| object | "contact" | Yes | |
| id | string | Yes | |
| string | Yes | ||
| first_name | string · nullable | Yes | |
| last_name | string · nullable | Yes | |
| company | string · nullable | Yes | |
| phone | string · nullable | Yes | |
| website | string · nullable | Yes | |
| string · nullable | Yes | ||
| string · nullable | Yes | ||
| string · nullable | Yes | ||
| status | string | Yes | |
| tags | string[] | Yes | |
| source | string · nullable | Yes | |
| custom_fields | object | Yes | open keys |
| emails_opened_count | number | Yes | |
| links_clicked_count | number | Yes | |
| total_interactions | number | Yes | |
| has_replied | boolean | Yes | |
| last_contacted_at | string · nullable | Yes | |
| last_activity_at | string · nullable | Yes | |
| created_at | string | Yes | |
| updated_at | string | Yes |
meta
| Field | Type | Always present | Notes |
|---|---|---|---|
| request_id | string | Yes |
updateContactUpdate a contact
Merge-patch: fields you omit are left alone, and null clears a field. tags and custom_fields are the exceptions worth knowing — tags REPLACES the whole array, while custom_fields merges key by key so a value written by an import is not lost.
/contacts/{contactId}contacts:writeExample request
curl -X PATCH "https://api.mailneo.co/api/v2/contacts/{contactId}" \
-H "X-API-Key: $MAILNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "<string>",
"first_name": "<string>",
"last_name": "<string>",
"company": "<string>",
"phone": "<string>",
"website": "<string>",
"linkedin": "<string>",
"twitter": "<string>",
"facebook": "<string>",
"source": "<string>",
"status": "ACTIVE",
"tags": [
"<string>"
],
"custom_fields": {}
}'Authorization
- Required scope
contacts: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 |
|---|---|---|---|
| contactId | 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 | |
| first_name | string · nullable | No | max 200 chars |
| last_name | string · nullable | No | max 200 chars |
| company | string · nullable | No | max 200 chars |
| phone | string · nullable | No | max 50 chars |
| website | string · nullable | No | max 500 chars |
| string · nullable | No | max 500 chars | |
| string · nullable | No | max 500 chars | |
| string · nullable | No | max 500 chars | |
| source | string · nullable | No | max 200 chars |
| status | "ACTIVE" | "CONVERTED" | "LOST" | "UNQUALIFIED" | "ARCHIVED" | 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 |
deleteContactDelete a contact
Permanent. The contact row is removed, and the copy of its name and email retained on campaign recipients is anonymised in the same transaction, so campaign counts survive and that copy does not. There is no undo — call with ?dry_run=true first. This is not a complete erasure request: sent messages retain the delivery address in the sending log. Use the account data-erasure flow for a full GDPR deletion.
/contacts/{contactId}contacts:deleteExample request
curl -X DELETE "https://api.mailneo.co/api/v2/contacts/{contactId}" \
-H "X-API-Key: $MAILNEO_API_KEY"Authorization
- Required scope
contacts: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 |
|---|---|---|---|
| contactId | 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 | "contact" | 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 |
bulkUpsertContactsCreate or update many contacts
Upserts up to 100 contacts by email, in one transaction: either every row is written or none is. Idempotency-Key is required, because a retried batch that ran the first time must not run twice. Existing contacts are merge-patched exactly as PATCH /contacts/{contactId} would patch them.
/contacts/bulkcontacts:writeExample request
curl -X POST "https://api.mailneo.co/api/v2/contacts/bulk" \
-H "X-API-Key: $MAILNEO_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{
"email": "<string>"
}
]
}'Authorization
- Required scope
contacts:write. A key without it is refused before the endpoint runs.- Credential
X-API-Key: mk_live_...Authorization: Bearer mk_live_...- Idempotency
Idempotency-Keyis required.
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 | Yes | 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
contacts[]Each element of the array.
| Field | Type | Always present | Notes |
|---|---|---|---|
| string | Yes | max 320 chars · pattern-constrained | |
| first_name | string · nullable | No | max 200 chars |
| last_name | string · nullable | No | max 200 chars |
| company | string · nullable | No | max 200 chars |
| phone | string · nullable | No | max 50 chars |
| website | string · nullable | No | max 500 chars |
| string · nullable | No | max 500 chars | |
| string · nullable | No | max 500 chars | |
| string · nullable | No | max 500 chars | |
| source | string · nullable | No | max 200 chars |
| status | "ACTIVE" | "CONVERTED" | "LOST" | "UNQUALIFIED" | "ARCHIVED" | No | |
| tags | string[] | No | |
| custom_fields | object | No | open keys |
Response · 200
Success
dataMay be null.
| Field | Type | Always present | Notes |
|---|---|---|---|
| object | "bulk_result" | Yes | |
| requested | integer | Yes | |
| created | integer | Yes | |
| updated | integer | Yes | |
| results | object[] | Yes | |
| results[].index | integer | Yes | |
| results[].id | string | Yes | |
| results[].email | string | Yes | |
| results[].operation | "created" | "updated" | 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 |