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 →