Skip to main content

Manage Contacts

Contacts represent the people your team communicates with across all messaging channels. This guide covers full CRUD operations, tagging, and bulk import.

Required Scopes

ScopePurpose
contacts:readList and view contacts
contacts:createCreate contacts and import CSV
contacts:updateUpdate contact details and add tags
contacts:deleteDelete contacts

Key Contact Fields

FieldDescription
idUnique contact identifier
nameDisplay name
emailEmail address
phonePhone number (E.164 format, e.g., +14155551234)
contact_methodsArray of contact methods (platform IDs like WhatsApp, Telegram, etc.)
custom_fieldsKey-value object for custom data
tagsArray of associated tags

List Contacts

GET /api/v1/contacts

Query Parameters

ParameterTypeDefaultDescription
searchstringSearch by name, email, or phone
tagstringFilter by tag ID
list_idstringFilter by list membership
pageinteger1Page number
page_sizeinteger20Items per page (max 100)

curl

curl -X GET "https://api.sendseven.com/api/v1/contacts?search=garcia&page=1&page_size=20" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"

Response

{
"items": [
{
"id": "contact_d4e5f6a7",
"name": "Maria Garcia",
"email": "[email protected]",
"phone": "+34612345678",
"contact_methods": [
{"id": "cm_wa_001", "method_type": "whatsapp_id", "value": "34612345678"}
],
"custom_fields": {
"company": "Acme Corp",
"role": "Product Manager"
},
"tags": [
{"id": "tag_vip", "name": "VIP", "color": "#EF4444"}
],
"created_at": "2026-01-10T08:00:00Z",
"updated_at": "2026-02-05T11:30:00Z"
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 2,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
}

Create a Contact

At least one identifier (email, phone, or platform ID) is required.

POST /api/v1/contacts

Request Body

FieldTypeRequiredDescription
namestringNoDisplay name
emailstringNoEmail address
phonestringNoPhone number in E.164 format
contact_methodsarrayNoArray of contact methods (e.g., [{"method_type": "whatsapp_id", "value": "1234567890"}])
custom_fieldsobjectNoKey-value pairs for custom data

curl

curl -X POST "https://api.sendseven.com/api/v1/contacts" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Sophie Martin",
"email": "[email protected]",
"phone": "+33612345678",
"contact_methods": [
{"method_type": "telegram_id", "value": "123456789"},
{"method_type": "whatsapp_id", "value": "33612345678"}
],
"custom_fields": {
"company": "TechStart SAS",
"source": "website_form"
}
}'

Python

import requests

BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}

# Create a contact
new_contact = requests.post(
f"{BASE_URL}/contacts",
headers=HEADERS,
json={
"name": "Alex Johnson",
"email": "[email protected]",
"phone": "+12025551234",
"custom_fields": {"source": "api", "plan": "enterprise"},
},
)
contact = new_contact.json()
print(f"Created: {contact['id']}")

# Search contacts
results = requests.get(
f"{BASE_URL}/contacts",
headers=HEADERS,
params={"search": "Johnson", "page_size": 10},
)
for c in results.json()["items"]:
print(f" {c['name']} - {c['email']}")

JavaScript

const BASE_URL = "https://api.sendseven.com/api/v1";
const HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
};

// Create a contact
const response = await fetch(`${BASE_URL}/contacts`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
name: "Alex Johnson",
email: "[email protected]",
phone: "+12025551234",
}),
});
const contact = await response.json();

// Update the contact
await fetch(`${BASE_URL}/contacts/${contact.id}`, {
method: "PUT",
headers: HEADERS,
body: JSON.stringify({
custom_fields: { company: "New Corp", role: "CTO" },
}),
});

Response (201 Created)

{
"id": "contact_f6a7b8c9",
"name": "Sophie Martin",
"email": "[email protected]",
"phone": "+33612345678",
"contact_methods": [
{"id": "cm_tg_001", "method_type": "telegram_id", "value": "123456789"},
{"id": "cm_wa_002", "method_type": "whatsapp_id", "value": "33612345678"}
],
"custom_fields": {
"company": "TechStart SAS",
"source": "website_form"
},
"tags": [],
"created_at": "2026-02-10T16:00:00Z",
"updated_at": "2026-02-10T16:00:00Z"
}

Get a Contact

GET /api/v1/contacts/{contact_id}
curl -X GET "https://api.sendseven.com/api/v1/contacts/contact_d4e5f6a7" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"

Update a Contact

All fields are optional. Only included fields are updated. Custom fields are merged with existing values.

PUT /api/v1/contacts/{contact_id}
curl -X PUT "https://api.sendseven.com/api/v1/contacts/contact_d4e5f6a7" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Maria Garcia-Lopez",
"custom_fields": {
"company": "Acme Corporation",
"role": "VP of Product"
}
}'

Delete a Contact

Permanently deletes a contact and disassociates them from all conversations.

DELETE /api/v1/contacts/{contact_id}
curl -X DELETE "https://api.sendseven.com/api/v1/contacts/contact_e5f6a7b8" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
{
"success": true,
"id": "contact_e5f6a7b8"
}

Add Tags to a Contact

POST /api/v1/contacts/{contact_id}/tags
curl -X POST "https://api.sendseven.com/api/v1/contacts/contact_d4e5f6a7/tags" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"tag_ids": ["tag_vip", "tag_enterprise"]
}'

Import Contacts (CSV)

Bulk import contacts from a CSV file.

POST /api/v1/contacts/import
curl -X POST "https://api.sendseven.com/api/v1/contacts/import" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-F "[email protected]" \
-F "list_id=list_newsletter"

CSV Format

name,email,phone,company,source
Sophie Martin,[email protected],+33612345678,TechStart SAS,import
James Wilson,[email protected],+447911123456,UK Solutions Ltd,import

Response

{
"imported": 2,
"skipped": 0,
"errors": [],
"total_rows": 2
}

If some rows fail validation:

{
"imported": 1,
"skipped": 1,
"errors": [
{
"row": 3,
"message": "Invalid phone number format"
}
],
"total_rows": 2
}

Duplicates & Merging

SendSeven detects duplicate contacts on every contact-creation path (REST, CSV import, live chat capture) using normalized phone, email, and platform identifiers. How a collision is handled depends on the duplicate mode in effect for that request.

Operating Modes

Three tenant-level modes control duplicate handling. The default for every account (existing and new) is auto_merge.

ModeBehavior
auto_mergeWhen a new contact has a phone, email, or platform ID that already exists in the tenant, the existing contact is updated instead of creating a duplicate. The oldest contact is kept as primary. Conflicting field values are recorded as a system note on the surviving contact.
allow_duplicatesA new contact is always created. The response includes a potential_duplicates array so the client can surface possible matches to the user.
dont_allow_duplicatesCreates that collide are rejected. The API returns 409 Conflict with the existing duplicates listed.

Change the tenant default in Settings → Contacts & Duplicates in the dashboard, or via the API:

curl -X PUT "https://api.sendseven.com/api/v1/tenants/{tenant_id}" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{ "duplicate_contact_mode": "allow_duplicates" }'

Per-Request Override

Every contact-creation endpoint accepts an optional duplicate_mode field that overrides the tenant default for that single request. If omitted, the tenant default is used.

EndpointOverride FieldLocation
POST /contactsduplicate_modeJSON body
POST /contacts/{id}/methodsduplicate_modeJSON body
POST /email-contacts/importduplicate_modeMultipart form field

Allowed values: auto_merge, allow_duplicates, dont_allow_duplicates.

Field-Merge Rules

When auto_merge triggers (or a manual merge is performed), scalar fields are reconciled with these rules:

  • Non-empty wins. If only one contact has a value, that value is kept.
  • Older contact wins ties. If both contacts have the same field set to different non-empty values, the value from the contact with the earlier created_at is kept.
  • is_blocked and is_archived: any-true wins. This is sticky for safety -- a blocked contact cannot be silently unblocked through a merge.
  • All conversations, messages, notes, tags, custom fields, and contact methods from the merged-away contact are moved to the survivor. Tag and list memberships are de-duplicated.
  • A system note is appended to the survivor summarizing every conflict, e.g. "Merged from contact 42c1.... Conflicts: first_name='Max' kept, discarded ['Maxi'].".

Old UUID Resolution

After a merge, the merged-away contact's UUID continues to resolve. GET /contacts/{old_id} returns:

  • HTTP 200 OK with the surviving contact's full body.
  • An additional merged_from object documenting the redirect.
  • A response header X-Merged-Into: {current_id} providing the new ID for clients that want to update their stored references.
curl -i -X GET "https://api.sendseven.com/api/v1/contacts/contact_old_b8c9d0e1" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
HTTP/1.1 200 OK
X-Merged-Into: contact_d4e5f6a7
Content-Type: application/json
{
"id": "contact_d4e5f6a7",
"name": "Maria Garcia",
"email": "[email protected]",
"phone": "+34612345678",
"merged_from": {
"old_id": "contact_old_b8c9d0e1",
"merged_at": "2026-04-12T10:23:00Z",
"reason": "auto"
},
"created_at": "2026-01-10T08:00:00Z",
"updated_at": "2026-04-12T10:23:00Z"
}

Inbound Channel Behavior

When an inbound message arrives via WhatsApp, Telegram, or another channel for a contact that already has a matching phone or email but does not yet have the channel's specific platform ID (e.g. whatsapp_id), the new contact method is attached to the existing contact regardless of the duplicate mode.

This is not a duplicate -- it is a new method on an existing contact. Inbound message processing is therefore mode-agnostic.

Example: Auto-Merge Create

A second create with the same phone returns the existing contact instead of creating a new one.

curl -X POST "https://api.sendseven.com/api/v1/contacts" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Maxi Mustermann",
"phone": "+491701234567"
}'
{
"id": "contact_d4e5f6a7",
"name": "Max Mustermann",
"email": "[email protected]",
"phone": "+491701234567",
"contact_methods": [
{"id": "cm_ph_001", "method_type": "phone", "value": "+491701234567"}
],
"tags": [],
"created_at": "2026-01-10T08:00:00Z",
"updated_at": "2026-04-12T10:23:00Z"
}

A system note is added to the contact:

Merged from contact 42c1bbcb-7a09-4a1a-9b0f-3fdaae8c6e2b. Conflicts: name='Max Mustermann' kept, discarded ['Maxi Mustermann'].

Example: Allow Duplicates Create

curl -X POST "https://api.sendseven.com/api/v1/contacts" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Maxi Mustermann",
"phone": "+491701234567",
"duplicate_mode": "allow_duplicates"
}'
{
"id": "contact_new_a9b8c7d6",
"name": "Maxi Mustermann",
"phone": "+491701234567",
"contact_methods": [
{"id": "cm_ph_044", "method_type": "phone", "value": "+491701234567"}
],
"potential_duplicates": [
{
"id": "contact_d4e5f6a7",
"name": "Max Mustermann",
"matched_on": ["phone"],
"methods": [
{"method_type": "phone", "value": "+491701234567"}
]
}
],
"created_at": "2026-04-12T11:05:00Z",
"updated_at": "2026-04-12T11:05:00Z"
}

Example: Don't Allow Duplicates (409)

curl -X POST "https://api.sendseven.com/api/v1/contacts" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Maxi Mustermann",
"phone": "+491701234567",
"duplicate_mode": "dont_allow_duplicates"
}'
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"detail": "duplicate_contact",
"duplicates": [
{
"id": "contact_d4e5f6a7",
"name": "Max Mustermann",
"matched_on": ["phone"],
"methods": [
{"method_type": "phone", "value": "+491701234567"}
]
}
]
}

To proceed despite the collision, retry with duplicate_mode: "allow_duplicates" or "auto_merge".

Manual Merge

Merge one or more secondary contacts into a primary contact. Requires scope contacts:update.

POST /api/v1/contacts/{id}/merge
curl -X POST "https://api.sendseven.com/api/v1/contacts/contact_d4e5f6a7/merge" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"secondary_ids": ["contact_old_b8c9d0e1", "contact_old_e1f2a3b4"],
"primary_override": false
}'

By default the oldest contact in the set becomes the primary regardless of the path id. Set primary_override: true to force the path id ({id}) to be the primary.

{
"contact": {
"id": "contact_d4e5f6a7",
"name": "Maria Garcia",
"email": "[email protected]",
"phone": "+34612345678",
"contact_methods": [
{"id": "cm_wa_001", "method_type": "whatsapp_id", "value": "34612345678"},
{"id": "cm_tg_017", "method_type": "telegram_id", "value": "987654321"}
],
"tags": [
{"id": "tag_vip", "name": "VIP", "color": "#EF4444"}
],
"created_at": "2026-01-10T08:00:00Z",
"updated_at": "2026-04-12T10:23:00Z"
},
"merged_count": 2
}

List Duplicate Groups

For bulk-merge UIs. Returns groups of contacts that share at least one of the requested signals. Requires scope contacts:read.

GET /api/v1/contacts/duplicates
ParameterTypeDefaultDescription
signalsstringphone,email,platform_idComma-separated list. Allowed: phone, email, platform_id, name. The name signal is exact match only and prone to false positives.
pageinteger1Page number
page_sizeinteger25Items per page (max 100)
curl -X GET "https://api.sendseven.com/api/v1/contacts/duplicates?signals=phone,email&page=1&page_size=25" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
{
"items": [
{
"signal": "phone",
"value": "+491701234567",
"contacts": [
{"id": "contact_d4e5f6a7", "name": "Max Mustermann", "created_at": "2026-01-10T08:00:00Z"},
{"id": "contact_new_a9b8c7d6", "name": "Maxi Mustermann", "created_at": "2026-04-12T11:05:00Z"}
]
}
],
"pagination": {
"page": 1,
"page_size": 25,
"total": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
}

Error Responses

StatusError CodeDescription
401INVALID_TOKENToken is invalid or expired
403INSUFFICIENT_SCOPEToken lacks required contacts scope
404RESOURCE_NOT_FOUNDContact ID does not exist or belongs to another tenant
409duplicate_contactA contact with the same identifier already exists and the request used dont_allow_duplicates
422VALIDATION_ERRORInvalid fields (e.g., malformed phone number or email)

Next Steps