Creating and changing contacts
A contact you create is handled like one that arrives from a sync. Here is where it goes, and what we do with it.
Where a created contact goes
We save the contact in one of your two-way accounts. We choose it: the account you linked first that keeps an address book and isn’t paused. Then the sync sends the contact to your other two-way accounts. It never sends it to a read-only account.
If you have no such account, the call fails with 422 no_account_to_write_to.
How a created contact is matched
We compare the new contact with the contacts you have, the way we compare any contact that arrives from an account. What happens next depends on how close the match is and on your Auto-Merge level. Each answer has an outcome:
| outcome | Status | What happened |
|---|---|---|
created | 201 | A new contact. It matched no contact you have. |
merged | 200 | The contact matched one you have, and your Auto-Merge level covers the match. The values were added to that contact. Nothing new was created. |
needs_review | 201 | A new contact. It matched contacts you have, but your Auto-Merge level doesn’t merge them. The pair waits in Review duplicates, and the answer lists the possible matches. |
updated | 200 | An existing contact was changed (PATCH). |
When a create is merged, the values you sent are added to the contact you already have. We don’t overwrite or remove a value it holds. History shows it as a change you made through the key, and you can roll it back like any change. It is not shown as a merge, and the contacts.merged trigger doesn’t fire for it.
A needs_review answer lists possibleMatches: each has a contact and a confidence from 0 to 100.
Over your plan’s contact limit
We never refuse a create because of your plan’s contact limit. Over the limit, the create still answers 201, and the new contact is frozen: the answer has "frozen": true.
{
"outcome": "created",
"contact": {
"id": "…",
"displayName": "Ada Lovelace",
"frozen": true
}
}ContactZapp keeps a frozen contact, but doesn’t send it to your accounts until you upgrade or free a place under the limit. frozen is on every contact the API returns, in reads and in trigger items, so you can see which contacts are held back. It is read-only: you can’t set it.
The fields of a contact
A write takes these fields, under the same names and in the same shapes that a read returns them. Everything else is read-only: id, publicId, displayName, updatedAt and frozen.
| Field | Type |
|---|---|
firstName | string |
middleName | string |
lastName | string |
prefix | string |
suffix | string |
nickname | string |
organization | string |
jobTitle | string |
emails | list of LabeledValue |
phones | list of LabeledValue |
addresses | list of LabeledValue |
urls | list of LabeledValue |
birthday | PartialDate or null |
notes | string |
labels | list of string |
An email, phone, address or url is { "label": "work", "value": "…" }. The label can be empty. preferred: true marks the one to call or write to; leave it out if your tool has no such idea. A date is { "year": 1815, "month": 12, "day": 10 }, and a part you don’t know is 0. A birthday with no year is { "year": 0, "month": 3, "day": 15 }.
If the body has a field that isn’t in the table, we refuse it with 400 unknown_field and write nothing. That includes the read-only fields. To change a contact, send only the fields you change, not the whole contact you read.
Changing a contact
PATCH /api/v1/contacts/{id} changes only the fields in the body. A field you leave out keeps its value. The change is written to every two-way account the contact is in, never to a read-only one. History records it as yours, via the key.
| In the body | What happens |
|---|---|
"jobTitle": "Analyst" | The job title is set. |
| A field that isn’t there | It keeps its value. |
"organization": "" or "phones": [] | The field is cleared. |
A list, such as emails | The list replaces the stored list. Send the whole list you want. |
"birthday": null | The birthday is cleared. A birthday that isn’t in the body is kept. A date whose year, month and day are all 0 clears it too. |
curl -s -X PATCH https://app.contactz.app/api/v1/contacts/772189b9-ef30-44a8-ace6-3955d061633c \
-H "Authorization: Bearer $CZ_KEY" \
-H "Content-Type: application/json" \
-d '{ "birthday": null }'null clears the birthday only. On any other field, null means “not sent”, so use "" or [] to clear it.
The id in the path is the contact’s id, or its publicId such as CS-0042.