Contacts
Contacts
A contact is a person or business in your CRM, addressed by its user-visible contactId such as CR-100042. Contacts are never addressed by an internal database id.
The contact shape
A contact is returned as a flat object. The important fields are:
| Field | Description |
|---|---|
contactId | User-visible id, for example CR-100042. |
identityType | personal or business. |
source | Where the contact came from, for example api. Nullable. |
firstName, lastName | Person name fields. Nullable. |
email, phone | Primary contact channels. Nullable. |
businessName, website | Set for business contacts. Nullable. |
address | Object with line1, line2, city, state, zip, country. |
contactType | The type object ({ id, name, category }). Nullable. |
tags | Array of { id, name } tag objects. |
subscribed, smsSubscribed | Email and SMS subscription booleans. |
notes | Free-text notes. Nullable. |
customFields | Map of custom-field id to value. |
createdAt, updatedAt | ISO 8601 timestamps. |
duplicateSignal | Present on GET by id; counts of email and phone matches. |
customFields is a flat map keyed by each custom field id, not by title. Discover the ids with GET /custom-fields.
Create a contact
POST /contacts always creates and never upserts. Requires contacts:write. When you omit contactTypeId the project default Customer type is used, and a businessName implies a business contact.
curl -X POST https://api.craftify.ai/v1/contacts \
-H "Authorization: Bearer cfy_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Nguyen",
"email": "jane@example.com",
"phone": "+16045551234",
"tags": ["vip"],
"customFields": { "665f3a1b2c3d4e5f6a7b8c9d": "High" }
}'To upsert, first GET /contacts?email= to check for an existing contact. If one is returned, PATCH it; otherwise POST a new one. Creating without this check can produce duplicates.
Update a contact
PATCH /contacts/{contactId} is a partial update. Only the fields you send change, and sending null clears a field. Include a new contactId in the body to relabel the contact, which must stay unique within the project. Requires contacts:write.
curl -X PATCH https://api.craftify.ai/v1/contacts/CR-100042 \
-H "Authorization: Bearer cfy_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "+16045559999",
"tags": ["vip", "newsletter"],
"notes": null
}'Get a contact
GET /contacts/{contactId} returns the full contact plus a read-time duplicateSignal. Requires contacts:read.
curl https://api.craftify.ai/v1/contacts/CR-100042 \
-H "Authorization: Bearer cfy_live_xxx"{
"contactId": "CR-100042",
"identityType": "personal",
"email": "jane@example.com",
"phone": "+16045559999",
"tags": [{ "id": "t_1", "name": "vip" }],
"duplicateSignal": { "emailMatches": 0, "phoneMatches": 1 }
}List and search
GET /contacts returns contacts sorted by most recently updated, using cursor pagination. Filters combine with AND. Requires contacts:read.
| Filter | Matches |
|---|---|
email | Exact, case-insensitive email. |
phone | Format-insensitive phone. |
contactTypeId | A specific contact type id. |
contactTypeCategory | Lead, Customer, Personnel, or Other. |
tag | Tag name or id; repeatable. |
updatedSince | ISO 8601 timestamp; updated at or after. |
search | Free-text over name, email, and contactId. |
curl "https://api.craftify.ai/v1/contacts?contactTypeCategory=Customer&tag=vip&limit=50" \
-H "Authorization: Bearer cfy_live_xxx"{
"data": [ { "contactId": "CR-100042", "email": "jane@example.com" } ],
"pagination": { "nextCursor": "eyJ1cGRhdGVkQXQiOiIuLi4ifQ", "hasMore": true }
}Fetch the next page by passing pagination.nextCursor as ?cursor=. On the last page nextCursor is null and hasMore is false.
Custom fields
Discover custom field ids with GET /custom-fields, then write values in the customFields map keyed by field id (the exact field title also works). For select and radio fields, send one of the configured option values.
Image and offering-list fields have writable: false and are read-only in v1. Writes to them are rejected.
Tags
The tags array on create and update accepts tag names or ids. Unknown names are created automatically, so you do not need to pre-create a tag before using it.
Deduplication
Use duplicateSignal from a GET by id to decide whether a contact overlaps with others. emailMatches and phoneMatches count other contacts that share this contact email or phone using the same matching the CRM uses. A non-zero count is a signal to merge or reconcile rather than create another record.
Related
- Interaction logs — record events on a contact timeline.
- API reference — full request and response schemas.