---
title: "Contacts"
description: "`contacts.list`, `get`, `create`, `update` and `delete`."
url: "https://openemail.uk/docs/sdk/contacts"
area: "SDK"
category: "Mailbox"
---

# Contacts

`contacts.list`, `get`, `create`, `update` and `delete`.

## Every method

**usage.ts**

```
const page = await openemail.contacts.list({ limit: 100 })
  const contact = await openemail.contacts.get('ada@example.com')

  const saved = await openemail.contacts.create({
    email: 'grace@example.com',
    name: 'Grace Hopper',
    notes: 'Met at the compiler workshop',
  })

  await openemail.contacts.update(saved.email, { notes: null })
  await openemail.contacts.delete(saved.email)

  console.log(page.items.length, page.hasMore, contact.source, contact.lastSeenAt)
```

Most recently seen first, with contacts that have never been mailed last. `source` is `auto` when the row was written because a member sent that address a message from the app composer, which is a materially different claim from somebody having saved it. Mail arriving from an address writes nothing, and neither does a send through this API.

The book belongs to the workspace rather than to one person, so a contact saved by any member is the contact every member and every key sees. `create` writes `source` as `manual` and puts the contact in the default audience as it is written. Name lists of your own in `audienceIds` to join them in the same call, which also needs `audiences:write`, or add the contact later with `openemail.audiences.addContact`.

> Addresses are stored lowercased and the client encodes the one you pass, so `A+B@example.com` reaches the right row. The address is the identity, so `update` cannot change it: moving a contact is a `delete` and a `create`.

## Parameters: contacts.list

- `limit` (number): How many contacts to return per page: an integer from 1 to 200, defaulting to 50. It is coerced, so `'100'` off a query string is fine, and a value outside the range is a 422 rather than a clamped one.
- `cursor` (string): The `nextCursor` from the previous page. Never build one yourself: a cursor naming a contact that no longer exists is a 400 `invalid_cursor`, which means your paging state is stale and the walk should restart without a cursor.
- `source` (ContactSource): `'manual'` for the contacts somebody saved on purpose, `'auto'` for the ones the app composer recorded. Leave it out for the whole book.

## Response: ContactResource

`contacts.list` resolves to a `Page<ContactResource>`, so the rows are on `page.items` and the walk follows `page.nextCursor` while `page.hasMore` is true. `get`, `create` and `update` each resolve to one `ContactDetailResource`, the same row plus `audiences`. The address book is unbounded, which is why this route pages rather than returning an array that silently stopped at 200.

- `object` ('contact'): Always the string `contact`, on the list rows as well as on `get`.
- `email` (string): The address, lowercased on write so `Bob@x.com` and `bob@x.com` are one contact, and the key every contacts method takes, since no contact id is exposed. Rows belong to the workspace rather than to the member or the key that wrote them, so every member and every key on the workspace reads and writes one address book.
- `name` (string | null): The display name. Null when no name has ever been recorded for the address. An automatic write carries one only when the header supplied something other than the address itself, and it can never overwrite a name the user typed.
- `source` ('manual' | 'auto' | (string & {})): `auto` means the row was written because the user sent mail to that address; `manual` means somebody entered it by hand, a materially different claim, and an upsert never downgrades `manual` back to `auto`. Mail arriving from an address writes no row at all, deliberately, so somebody who has only ever written to you is not in here; the union stays open because the column is free text defaulting to `manual`.
- `notes` (string | null): Free text somebody wrote about this person, in the app or through `update`, never generated. Null when nobody has written any, and an explicit null on `update` clears it.
- `lastSeenAt` (string | null): ISO-8601 UTC, bumped every time a member sends to that address from the app composer, not when mail arrives from it, which writes nothing. Null on a contact saved through `create` that has never been mailed, and those sort last in the descending `lastSeenAt` order this route returns.
- `audiences` (Array<ContactAudienceResource>): Only on `get`, `create` and `update`, never on list rows. Every audience the contact is in as `{ id, name, builtin }`, the default one included. `builtin` is `default` on the audience every contact belongs to and null on one somebody created, so branch on it rather than on the name, which anybody can change.
