Contacts
Who a workspace knows how to reach - the telephony identity an inbound call resolves to.
Who a workspace already knows how to reach.
const { contact } = await connect.contacts.lookupByPhone({ workspaceId, phone });
if (contact) {
// A caller this workspace has met before.
}This is the telephony identity, not a CRM record. A contact here is keyed by primaryPhone - the one fact every inbound call carries - and it is written automatically: a voice agent creates one on the first call from a new number and fills fields in as it learns them. source tells you which kind you are looking at, and it is worth rendering: call means an agent guessed the details and nobody has confirmed them, manual means a person typed them.
Reference it, do not copy it. If your application keeps its own customer records - a CRM contact, a patient, a member - store this contact's id on yours rather than duplicating the fields. Two writable copies of a person's name with no single owner drift, and Connect's copy is the one an agent will keep updating from calls. Storing the id also gives you an exact join to anything else keyed by it: a booking made during a call carries contactId, so "which of my records is this appointment for" stops being a phone-number match and becomes a lookup.
Contacts are strictly per-workspace. A number known to two of your customers is two independent records with two independent histories - there is no cross-tenant identity graph, deliberately, because the only thing one could offer is telling one customer what another knows about a caller.
contact: null is the ordinary answer for a number nobody has called from before. It is not an error.
Endpoints
GET /api/v1/application/contacts
Look a caller up by phone number
Scope contacts:read · Rate limit 60 requests / min per key
"Do we already know this number?" - one point read, cheap enough for a screen pop on every inbound call.
Implementation
1. Prerequisites - an API key scoped to contacts:read, and a live subscription to your application in that workspace.
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/contacts?workspaceId=WORKSPACE_ID&phone=%2B919000000001" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"The number is normalized before the lookup, so +919000000001, 919000000001 and +91 90000 00001 all resolve to the same contact. Comparing unnormalized is how a contact that exists reads as absent.
3. contact: null is the ordinary answer for a number nobody has called from before - a first-time caller, not a failure.
4. Reference it, do not copy it. If you keep your own customer records, store this contact's id on yours. Connect's copy is the one a voice agent keeps updating from calls; a second writable copy of a person's name with no single owner drifts. Storing the id also makes Booking.contactId an exact join, so "which of my records is this appointment for" is a lookup rather than a phone match.
source is worth rendering: call means an agent filled these in and nobody has confirmed them.
Errors
validation_error (400 - no phone), forbidden (403 - key missing contacts:read, or your application is not installed in that workspace), rate_limited (429).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
workspaceId | query | yes | |
phone | query | yes |
Responses
| Status | Meaning |
|---|---|
200 | The contact, or null |
400 | validation_error |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
contact | any | yes |
POST /api/v1/application/contacts
Save what an interaction learned about a contact
Scope contacts:write · Rate limit 120 requests / min per key
Find the workspace's contact by phone, or create one, and fold in what a call or conversation learned - the write a voice agent makes when a caller gives their name.
Implementation
1. Prerequisites - an API key scoped to contacts:write, and a live subscription to your application in that workspace. Server-to-server only: a session token is refused.
2. Send the facts:
curl -X POST "https://connect.cosmoops.com/api/v1/application/contacts" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
-H "Content-Type: application/json" \
-d '{"workspaceId":"WORKSPACE_ID","phone":"+919000000001","source":"extraction","callId":"CALL_ID","facts":{"name":{"display":"Priya Sharma"},"business":{"company":"Acme Traders"}}}'3. Automated facts fill blanks; they never overwrite. source is call (what the telephony layer knows) or extraction (what a model inferred). Either way a value somebody typed by hand wins, a phone or email is never marked verified, and an array is added to rather than replaced. changedFields is what actually landed - a value the record already held is not rewritten - so answer the caller from it, not from what you sent.
4. Count the call once. countCall defaults to false. Pass true only on the one write that closes the interaction; every save made while it is still running leaves it off, or a contact who phoned once reads as having phoned three times.
contact.created or contact.updated (with changed) reaches the workspace's applications when something changed. A write that changed nothing announces nothing.
Errors
validation_error (400 - phone must be E.164), forbidden (403), rate_limited (429).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | |
phone | string | yes | |
facts | object | yes | |
source | call | extraction | yes | |
callId | any | no | |
countCall | boolean | no |
Responses
| Status | Meaning |
|---|---|
200 | An existing contact, after the merge |
201 | A contact this write created |
400 | validation_error |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
contact | object | yes | |
created | boolean | yes | |
changedFields | array | yes |
GET /api/v1/application/contacts/{contactId}
Read one contact
Scope contacts:read · Rate limit 60 requests / min per key
The record behind an id you already hold - the one you stored on your own customer record, or the contactId on a booking.
curl "https://connect.cosmoops.com/api/v1/application/contacts/CONTACT_ID?workspaceId=WORKSPACE_ID" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"A contact belongs to exactly one workspace, so an id from another tenant does not resolve - it answers 404, the same as an id that never existed.
Errors
forbidden (403), not_found (404 - no such contact in that workspace), rate_limited (429).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
contactId | path | yes |
Responses
| Status | Meaning |
|---|---|
200 | The contact |
403 | forbidden |
404 | not_found |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | |
primaryPhone | string | yes | |
phones | array | yes | |
emails | array | yes | |
name | object | yes | |
business | object | yes | |
tags | array | yes | |
source | call | manual | import | api | extraction | yes | |
createdAt | string | yes | |
updatedAt | string | yes |