cirrux

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

FieldTypeRequiredDescription
objectstringnoAlways addressbook.
uuidstringnoThe addressbook.
mailbox_uuidstringnoThe mailbox it belongs to.
mailbox_addressstring | nullnoThat mailbox’s email address. Every mailbox calls its addressbook "Contacts", and this tells them apart.
namestringnoThe addressbook’s name.
is_defaultbooleannoWhether this is the addressbook its mailbox files new contacts in.
created_atstringnoWhen it was created.
updated_atstring | nullnoWhen it last changed.

The contact object

FieldTypeRequiredDescription
objectstringnoAlways contact.
uuidstringnoThe contact.
addressbook_uuidstringnoThe addressbook it lives in.
kindstringnoindividual, organization, group, location or application.
name_formattedstring | nullnoThe full name as shown, such as "Jane Doe".
name_givenstring | nullnoGiven (first) name.
name_familystring | nullnoFamily (last) name.
name_additionalstring | nullnoAdditional (middle) names.
name_prefixstring | nullnoHonorific prefix, such as "dr.".
name_suffixstring | nullnoHonorific suffix.
nicknamestring | nullnoA nickname.
companystring | nullnoWhere they work.
titlestring | nullnoJob title.
rolestring | nullnoRole.
notesstring | nullnoFree text notes.
birthdayobject | nullnoAn object with year, month and day, where year is null when it isn’t known.
emailsarraynoEmail addresses, as below.
phonesarraynoPhone numbers, as below.
addressesarraynoPostal addresses, as below.
linksarraynoWebsites, chat addresses and social profiles, as below.
created_atstringnoWhen it was created.
updated_atstring | nullnoWhen 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:

FieldTypeRequiredDescription
emails[].emailstringnoThe email address.
phones[].phonestringnoThe phone number, as written.
addresses[]stringsnostreet, extended_address, po_box, city, state, postal_code and country, each null when empty.
links[].kindstringnourl for a website, impp for a chat address such as xmpp:jane@example.com, or social_profile.
links[].urlstringnoThe address itself.
links[].servicestring | nullnoFor a chat address or a social profile, the service, such as skype or twitter.
links[].labelstring | nullnoFor 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 uuid it 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 GET returned it, with your changes made: object, the timestamps and the contact’s own uuid and addressbook_uuid are accepted and ignored. Any other field the API doesn’t know is refused rather than ignored, so a typo like phone_numbers can’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 with https:// or http://, a chat address needs its scheme (xmpp:), and a social profile needs a service. 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

GET/v1/addressbooks

Returns 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

GET/v1/addressbooks/{addressbook_uuid}/contacts

Returns the contacts in one addressbook, ordered by name. Paginated: follow next_cursor while has_more is true. Scope contacts.read.

FieldTypeRequiredDescription
limitnumbernoContacts per page, 1 to 100. Defaults to 25.
cursorstringnoThe next_cursor of the previous page.

Search contacts

GET/v1/search/contacts

Finds 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.

FieldTypeRequiredDescription
qstringyesWhat to look for.
addressbook_uuidstringnoOnly this addressbook.
limitnumbernoContacts per page, 1 to 100. Defaults to 25.
cursorstringnoThe 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

GET/v1/contacts/{uuid}

Returns one contact. Someone else’s contact is 404 not_found. Scope contacts.read.

Create a contact

POST/v1/addressbooks/{addressbook_uuid}/contacts

Creates 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

POST/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

DELETE/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:

StatuserrorWhen
403forbiddenThe token is a workspace API key. Contacts need a user-scoped OAuth token.
404not_foundThe contact or addressbook does not exist or is not yours.
402subscription_requiredThe workspace needs an active subscription to create contacts.
400invalid_bodyThe body is not a JSON object.
422invalid_valueThe 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.
422invalid_queryA search without q.
422invalid_uuidA uuid in the path or query is not a valid UUID.