Contacts API
List a user’s addressbooks, find and read their contacts, and create, change and delete contacts. Every mailbox comes with an addressbook called Contacts, and a contact always lives in one addressbook. All endpoints are under https://api.cirrux.co/public_api/v1.
Every endpoint acts on one person’s contacts, so it needs a user-scoped OAuth token. A workspace API key is refused with 403 forbidden. Reading needs contacts.read, and creating, changing or deleting needs contacts.write. Changes made through the API reach the Cirrux apps and every CardDAV client, such as Apple Contacts.
The addressbook object
| Field | Type | Required | Description |
|---|---|---|---|
| object | string | no | Always addressbook. |
| uuid | string | no | The addressbook. |
| mailbox_uuid | string | no | The mailbox it belongs to. |
| mailbox_address | string | null | no | That mailbox’s email address. Every mailbox calls its addressbook "Contacts", and this tells them apart. |
| name | string | no | The addressbook’s name. |
| is_default | boolean | no | Whether this is the addressbook its mailbox files new contacts in. |
| created_at | string | no | When it was created. |
| updated_at | string | null | no | When it last changed. |
The contact object
| Field | Type | Required | Description |
|---|---|---|---|
| object | string | no | Always contact. |
| uuid | string | no | The contact. |
| addressbook_uuid | string | no | The addressbook it lives in. |
| kind | string | no | individual, organization, group, location or application. |
| name_formatted | string | null | no | The full name as shown, such as "Jane Doe". |
| name_given | string | null | no | Given (first) name. |
| name_family | string | null | no | Family (last) name. |
| name_additional | string | null | no | Additional (middle) names. |
| name_prefix | string | null | no | Honorific prefix, such as "dr.". |
| name_suffix | string | null | no | Honorific suffix. |
| nickname | string | null | no | A nickname. |
| company | string | null | no | Where they work. |
| title | string | null | no | Job title. |
| role | string | null | no | Role. |
| notes | string | null | no | Free text notes. |
| birthday | object | null | no | An object with year, month and day, where year is null when it isn’t known. |
| emails | array | no | Email addresses, as below. |
| phones | array | no | Phone numbers, as below. |
| addresses | array | no | Postal addresses, as below. |
| links | array | no | Websites, chat addresses and social profiles, as below. |
| created_at | string | no | When it was created. |
| updated_at | string | null | no | When it last changed. |
Every entry in the four lists has a uuid, types (such as work, home or cell; contacts from Apple and older apps also carry internet or voice) and a preference, where 1 marks the preferred one. On top of that:
| Field | Type | Required | Description |
|---|---|---|---|
| emails[].email | string | no | The email address. |
| phones[].phone | string | no | The phone number, as written. |
| addresses[] | strings | no | street, extended_address, po_box, city, state, postal_code and country, each null when empty. |
| links[].kind | string | no | url for a website, impp for a chat address such as xmpp:jane@example.com, or social_profile. |
| links[].url | string | no | The address itself. |
| links[].service | string | null | no | For a chat address or a social profile, the service, such as skype or twitter. |
| links[].label | string | null | no | For a website, what it is, such as Homepage. |
{
"object": "contact",
"uuid": "5e6f7a8b-1111-4c2d-8e3f-000000000001",
"addressbook_uuid": "0a1b2c3d-2222-4e5f-9a6b-000000000002",
"kind": "individual",
"name_formatted": "Jane Doe",
"name_given": "Jane",
"name_family": "Doe",
"company": "Acme",
"title": "CFO",
"birthday": { "year": null, "month": 3, "day": 14 },
"emails": [
{ "object": "contact_email", "uuid": "7c8d9e0f-…",
"email": "jane@acme.example", "types": ["work"],
"preference": 1 }
],
"phones": [
{ "object": "contact_phone", "uuid": "1a2b3c4d-…",
"phone": "+31 6 1234 5678", "types": ["cell"],
"preference": 1 }
],
"addresses": [
{ "object": "contact_address", "uuid": "9f8e7d6c-…",
"street": "Keizersgracht 1", "extended_address": null,
"po_box": null, "city": "Amsterdam", "state": null,
"postal_code": "1015 CJ", "country": "NL",
"types": ["work"], "preference": 1 }
],
"links": [
{ "object": "contact_link", "uuid": "4b5c6d7e-…", "kind": "url",
"url": "https://acme.example", "service": null,
"label": "Homepage", "preference": null }
],
"created_at": "2026-10-20T09:12:44.512Z",
"updated_at": "2026-10-28T15:01:02.118Z"
}How writes treat the lists
A contact is written as one object, the same shape it is read as. There are no separate endpoints for its phone numbers or addresses.
- A list you send replaces the contact’s whole list. To add a phone number, send every phone number the contact already has plus the new one. A list you leave out stays as it is, and
[]clears it. - Send each entry back with its uuid. An entry with the
uuidit was read with is changed in place, so fixing a typo in a number keeps it the same entry. One without a uuid is matched to an existing entry with exactly the same value, or added. A uuid that isn’t one of this contact’s own entries is refused. - Read, edit, send back. You can post a contact exactly as
GETreturned it, with your changes made:object, the timestamps and the contact’s ownuuidandaddressbook_uuidare accepted and ignored. Any other field the API doesn’t know is refused rather than ignored, so a typo likephone_numberscan’t lose data quietly. - Values are checked. A new email address must be a whole address, a phone number digits with spaces and
+ - ( ) . /, a website must start withhttps://orhttp://, a chat address needs its scheme (xmpp:), and a social profile needs aservice. A value the contact already has is kept as it is, even when it wouldn’t pass, so a contact from Apple Contacts or an import can always be sent back. - When no entry in an email, phone or address list has a
preference, the first one becomes the preferred one. - A contact always needs a name, a company or an email address. Creating one without is refused, and so is a change that would take the last of them away.
Endpoints
List addressbooks
/v1/addressbooksReturns your addressbooks in the token’s workspace, ordered by name. Not paginated. Scope contacts.read.
curl https://api.cirrux.co/public_api/v1/addressbooks \ -H "Authorization: Bearer $CIRRUX_TOKEN"
List the contacts in an addressbook
/v1/addressbooks/{addressbook_uuid}/contactsReturns the contacts in one addressbook, ordered by name. Paginated: follow next_cursor while has_more is true. Scope contacts.read.
| Field | Type | Required | Description |
|---|---|---|---|
| limit | number | no | Contacts per page, 1 to 100. Defaults to 25. |
| cursor | string | no | The next_cursor of the previous page. |
Search contacts
/v1/search/contactsFinds contacts whose name, company or email address contains the query, anywhere and regardless of case or accents: “jan” finds Janssen, and “zoe” finds Zoë. Phone numbers are not searched. Covers every addressbook you own in the workspace unless you name one. Ordered by name and paginated like the list. Scope contacts.read.
| Field | Type | Required | Description |
|---|---|---|---|
| q | string | yes | What to look for. |
| addressbook_uuid | string | no | Only this addressbook. |
| limit | number | no | Contacts per page, 1 to 100. Defaults to 25. |
| cursor | string | no | The next_cursor of the previous page. |
curl -G https://api.cirrux.co/public_api/v1/search/contacts \ --data-urlencode q=jane \ -H "Authorization: Bearer $CIRRUX_TOKEN"
Get a contact
/v1/contacts/{uuid}Returns one contact. Someone else’s contact is 404 not_found. Scope contacts.read.
Create a contact
/v1/addressbooks/{addressbook_uuid}/contactsCreates a contact in one of your addressbooks and returns 201 with the contact object. Takes the fields of the contact object, including the four lists. name_formatted is made from the given and family names when you leave it out. birthday also takes 1980-03-14, or --03-14 without a year. Scope contacts.write.
curl \
https://api.cirrux.co/public_api/v1/addressbooks/…/contacts \
-H "Authorization: Bearer $CIRRUX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name_given": "Jane",
"name_family": "Doe",
"company": "Acme",
"emails": [
{ "email": "jane@acme.example", "types": ["work"] }
],
"phones": [
{ "phone": "+31 6 1234 5678", "types": ["cell"] }
],
"birthday": "--03-14"
}'Change a contact
/v1/contacts/{uuid}Changes the fields you send and returns the contact. A field you leave out stays as it is, and null clears it, birthday included. Lists follow the rules above, so to add a phone number, send them all. A contact can’t move to another addressbook. Scope contacts.write.
curl https://api.cirrux.co/public_api/v1/contacts/5e6f7a8b-… \
-H "Authorization: Bearer $CIRRUX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "CEO",
"phones": [
{ "uuid": "1a2b3c4d-…", "phone": "+31 6 1234 5678",
"types": ["cell"] },
{ "phone": "+31 20 765 4321", "types": ["work"] }
]
}'Delete a contact
/v1/contacts/{uuid}Deletes a contact with its email addresses, phone numbers, postal addresses and links, from every app and device it syncs to. Returns 204. Scope contacts.write.
Errors
Contact error codes
On top of the shared errors, contact endpoints can return:
| Status | error | When |
|---|---|---|
| 403 | forbidden | The token is a workspace API key. Contacts need a user-scoped OAuth token. |
| 404 | not_found | The contact or addressbook does not exist or is not yours. |
| 402 | subscription_required | The workspace needs an active subscription to create contacts. |
| 400 | invalid_body | The body is not a JSON object. |
| 422 | invalid_value | The body has an unknown field, a value that is not an email address, phone number, web or chat address, a birthday that is not a real date, a uuid that is not one of this contact’s entries, nothing to change, or would leave the contact without a name, company or email address. The error_description says which. |
| 422 | invalid_query | A search without q. |
| 422 | invalid_uuid | A uuid in the path or query is not a valid UUID. |