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
| Scope | Purpose |
|---|---|
contacts:read | List and view contacts |
contacts:create | Create contacts and import CSV |
contacts:update | Update contact details and add tags |
contacts:delete | Delete contacts |
Key Contact Fields
| Field | Description |
|---|---|
id | Unique contact identifier |
name | Display name |
email | Email address |
phone | Phone number (E.164 format, e.g., +14155551234) |
contact_methods | Array of contact methods (platform IDs like WhatsApp, Telegram, etc.) |
custom_fields | Key-value object for custom data |
tags | Array of associated tags |
List Contacts
GET /api/v1/contacts
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search | string | Search by name, email, or phone | |
tag | string | Filter by tag ID | |
list_id | string | Filter by list membership | |
page | integer | 1 | Page number |
page_size | integer | 20 | Items 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Display name |
email | string | No | Email address |
phone | string | No | Phone number in E.164 format |
contact_methods | array | No | Array of contact methods (e.g., [{"method_type": "whatsapp_id", "value": "1234567890"}]) |
custom_fields | object | No | Key-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.
| Mode | Behavior |
|---|---|
auto_merge | When 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_duplicates | A new contact is always created. The response includes a potential_duplicates array so the client can surface possible matches to the user. |
dont_allow_duplicates | Creates 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.
| Endpoint | Override Field | Location |
|---|---|---|
POST /contacts | duplicate_mode | JSON body |
POST /contacts/{id}/methods | duplicate_mode | JSON body |
POST /email-contacts/import | duplicate_mode | Multipart 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_atis kept. is_blockedandis_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 OKwith the surviving contact's full body. - An additional
merged_fromobject 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
| Parameter | Type | Default | Description |
|---|---|---|---|
signals | string | phone,email,platform_id | Comma-separated list. Allowed: phone, email, platform_id, name. The name signal is exact match only and prone to false positives. |
page | integer | 1 | Page number |
page_size | integer | 25 | Items 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
| Status | Error Code | Description |
|---|---|---|
| 401 | INVALID_TOKEN | Token is invalid or expired |
| 403 | INSUFFICIENT_SCOPE | Token lacks required contacts scope |
| 404 | RESOURCE_NOT_FOUND | Contact ID does not exist or belongs to another tenant |
| 409 | duplicate_contact | A contact with the same identifier already exists and the request used dont_allow_duplicates |
| 422 | VALIDATION_ERROR | Invalid fields (e.g., malformed phone number or email) |
Next Steps
- Tags & Lists -- organize contacts with tags and lists
- Custom Fields -- store custom data on contacts
- Send Text Messages -- send messages to contacts