Contacts
Contacts are the people your campaigns call. Each contact carries tags for campaign targeting, a preferred language, and an arbitrary metadata object for lead data (cart contents, product interest, budget signals, etc.).
The public API uses the path /v1/contacts. The internal API uses /api/voice/customers — these are the same records.
List contacts​
GET /v1/contacts
Scope: voice:contacts:read
Query parameters:
| Param | Type | Notes |
|---|---|---|
search | string | Searches name and phone |
tags | string (repeatable) | ?tags=Region:West&tags=Segment:Lapsed |
limit | integer | Default 50, max 100 |
offset | integer | Default 0 |
Response:
{
"contacts": [
{
"uuid": "cst_c1d2e3",
"name": "Jordan Ellis",
"phone": "+14155550192",
"email": "[email protected]",
"tags": ["Region:West", "Segment:Lapsed"],
"preferred_language": "en",
"metadata": {
"product_interest": "Standing Desk Pro",
"cart_value": 850,
"lead_type": "cart_abandon"
},
"total_calls": 2,
"last_contact": "2026-05-12T14:34:00Z",
"created_at": "2026-05-01T00:00:00Z"
}
],
"total": 1482
}
Create contact​
POST /v1/contacts — Rate limit: 60/minute
Scope: voice:contacts:write
Request body:
{
"name": "Jordan Ellis",
"phone": "+14155550192",
"email": "[email protected]",
"tags": ["Region:West", "Segment:Lapsed"],
"preferred_language": "en",
"metadata": {
"product_interest": "Standing Desk Pro",
"cart_value": 850,
"lead_type": "cart_abandon"
}
}
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | |
phone | string | ✓ | E.164 format; upsert key |
email | string | — | |
tags | string[] | — | Used for campaign targeting |
preferred_language | string | — | hi, en, hi-en, ta, te, kn, mr, bn, gu, ml, pa |
metadata | object | — | Arbitrary JSON; carries lead data |
overwrite | boolean | — | Default false. If true, upserts on duplicate phone; if false, returns 409 Conflict |
Update contact​
PATCH /v1/contacts/{contact_id} — Rate limit: 60/minute
Scope: voice:contacts:write
All fields optional. metadata is merged, not replaced — pass only the keys you want to change.
{
"tags": ["Region:West", "Priority:High"],
"metadata": { "cart_value": 920 }
}
Delete contact​
DELETE /v1/contacts/{contact_id}
Scope: voice:contacts:write
Bulk upsert contacts​
POST /v1/contacts/bulk — Rate limit: 10/minute
Scope: voice:contacts:write
Upserts a batch of up to 1,000 contacts by phone number. Designed for daily lead-list pushes from external CRMs.
Request body:
{
"contacts": [
{
"name": "Jordan Ellis",
"phone": "+14155550192",
"tags": ["Region:West", "Segment:Lapsed"],
"metadata": { "cart_value": 850 }
}
],
"mode": "upsert"
}
| Field | Notes |
|---|---|
contacts | Up to 1,000 records per request |
mode | upsert (merge by phone) or append (always create new) |
Response:
{
"created": 218,
"updated": 66,
"errors": [
{ "row": 4, "phone": "+10000000000", "reason": "invalid phone format" }
]
}
One bad row does not fail the batch. Paginate for lists larger than 1,000.
Import from file​
POST /api/voice/customers/import — Rate limit: 5/minute
Imports from a CSV or JSON file. Upserts by phone number.
Request: multipart/form-data
file—.csvor.json
CSV format:
name,phone,email,tags,preferred_language,metadata
Jordan Ellis,+14155550192,[email protected],"Region:West,Segment:Lapsed",en,"{""cart_value"":850}"
Response:
{ "imported": 284, "updated": 41, "skipped": 2, "errors": [] }
Metadata endpoints​
| Endpoint | Returns |
|---|---|
GET /api/voice/customers/meta/tags | All unique tag values in the organisation |
GET /api/voice/customers/meta/branches | Tags prefixed with Branch: |
GET /api/voice/customers/meta/count?tags=... | Count of contacts matching given tags |