Contacts
Manage the people your agents call.
A contact is a person in your workspace, identified by your own externalRef. Contacts carry a phone number, preferences, opt-out state, and arbitrary attributes.
The contact object:
{
"id": "con_…",
"externalRef": "your-record-id",
"phone": "+919812345678",
"dnd": false,
"optOut": false,
"preferredLanguage": "hi",
"lastContactAt": "2026-08-20T12:00:00Z",
"attributes": { "plan": "gold" },
"name": "Nalin",
"email": null,
"company": null,
"whatsappUserId": null,
"whatsappUsername": null
}
whatsappUserId and whatsappUsername are set for a WhatsApp customer who hides their number (for example IN.13491208655302741918 and asha_k); phone is null until WhatsApp shares it.
dnd reflects the platform do-not-call registry and lastContactAt are read-only (set by the system); everything else you control.
List contacts
GET /v1/contacts · scope contacts:read · query limit (1–200, default 50), cursor
{ "data": [ { "id": "con_…", "externalRef": "…", "phone": "+91…", … } ], "nextCursor": "MjAyNi0…" }
Newest first. Pass nextCursor back as ?cursor= for the next page; it’s null on the last page.
Get a contact
GET /v1/contacts/:id · scope contacts:read
Returns the contact object plus doNotCall: whether calls to this contact are blocked, either because the contact opted out (optOut) or because its number is on your workspace’s do-not-call list (for example, the person asked on a call not to be called again).
{ "data": { "id": "bor_…", "externalRef": "crm-123", "phone": "+9198…", "optOut": false, "doNotCall": true, … } }
For the contact’s calls, use GET /v1/calls?contactId=….
Unknown id → 404. A contact erased under DPDP → 410 contact_erased.
Create or update (upsert)
POST /v1/contacts · scope contacts:write
Upserts by externalRef within your workspace: if a contact with that ref exists it’s updated (200), otherwise it’s created (201). On an update, any optional field you leave out keeps its current value - the defaults below apply only when a contact is created.
Body
| Field | Type | Required | Default / constraints |
|---|---|---|---|
externalRef |
string | yes | your id, 1–120 chars (immutable identity) |
phone |
string | yes | 8–20 chars, normalized to +91…. A contact’s number can’t change once set (400 phone_immutable); a number another contact owns is 409 with existingId. |
preferredLanguage |
enum | no | en or hi - new contacts default to en |
timezone |
string | no | new contacts default to Asia/Kolkata |
attributes |
object | no | arbitrary key/values - new contacts default to {} |
optOut |
boolean | no | true adds the number to your do-not-call list. false can’t lift an opt-out (409 opt_out_downgrade) - only the console can. |
Response
201 (created) or 200 (updated): { "data": contact }.
Batch upsert
POST /v1/contacts/batch · scope contacts:write
Up to 500 contacts per request, each handled exactly like create or update:
{ "contacts": [ { "externalRef": "crm-1", "phone": "+919812345678", "attributes": { "name": "Asha" } }, { "externalRef": "crm-2", "phone": "…" } ] }
Returns 200 with one result per item, in the same order. One bad item never fails the others:
{
"data": {
"succeeded": 1, "failed": 1,
"results": [
{ "status": 201, "externalRef": "crm-1", "data": { "id": "bor_…", … } },
{ "status": 409, "externalRef": "crm-2", "error": "a contact with this phone number already exists", "code": "conflict", "existingId": "bor_…" }
]
}
}
Each item’s status is what POST /v1/contacts would have returned for it: 201 created, 200 updated, 4xx refused.
Update a contact
PUT /v1/contacts/:id · scope contacts:write
Same fields as upsert except externalRef (identity can’t change). Response 200: { "data": contact }. Unknown id → 404 contact not found.
Delete a contact
DELETE /v1/contacts/:id · scope contacts:write → { "data": { "id": "con_…", "deleted": true } }.
409 contact has call history - cannot delete- a contact with calls can’t be hard-deleted. Use erase instead.404 contact not found
Do-not-call list
Your workspace’s do-not-call list. No call (API, campaign, or console) goes to a number on it. People are added automatically when they opt out on a call, and you can add numbers yourself.
List
GET /v1/dnc · scope contacts:read
Newest first. Query: phone (check one number), limit (1–200, default 50), offset.
{ "data": [ { "phone": "+9198…", "reason": "customer request", "createdAt": "…" } ], "total": 41 }
Add numbers
POST /v1/dnc · scope contacts:write
{ "phones": ["+919812345678", "98123 45679"], "reason": "customer request" }
Up to 1,000 numbers per request. If a number belongs to a contact, that contact is also marked opted out and a contact.opted_out webhook is sent.
{ "data": { "added": 2, "alreadyListed": 0, "invalid": [] } }
Numbers that aren’t valid Indian +91 numbers come back in invalid and are skipped. If none are valid, the request fails with 400 no_valid_numbers.
There is no API to remove a number: taking someone off the do-not-call list has to be done in the console.
Erase a contact (DPDP)
POST /v1/contacts/:id/erase · scope contacts:erase · no body
Erasure is irreversible, so it needs its own contacts:erase scope - a key with contacts:write alone can’t erase anyone.
Honors a data-subject erasure request: it wipes the contact’s personal data - identity, transcripts, and recordings - while keeping non-personal compliance proof. Unlike delete, erase works even when the contact has call history, and it’s idempotent.
Response 200: { "data": <erasure result> }. Unknown id → 404.
Next: Campaigns →