---
title: "Contacts"
description: "`contacts.list`, `get`, `create`, `save`, `update`, `set_audiences`, `delete`, `delete_many`, `list_people`, `set_photo`, `remove_photo`, `block`, `unblock`, `list_threads` and `activity`."
url: "https://openemail.uk/docs/ruby/contacts"
area: "Ruby"
category: "Mailbox"
---

# Contacts

`contacts.list`, `get`, `create`, `save`, `update`, `set_audiences`, `delete`, `delete_many`, `list_people`, `set_photo`, `remove_photo`, `block`, `unblock`, `list_threads` and `activity`.

## Every method

**usage.rb**

```
page = client.contacts.list(limit: 100)
contact = client.contacts.get("ada@example.com")

saved = client.contacts.create(
  email: "grace@example.com",
  name: "Grace Hopper",
  notes: "Met at the compiler workshop"
)

client.contacts.update("grace@example.com", notes: nil)
client.contacts.set_audiences("grace@example.com", audienceIds: ["aud_4c1b8e2a7d9f05c36b4e8a71"])
client.contacts.delete("grace@example.com")

puts page.items.size, page.has_more?, contact[:source], contact[:lastSeenAt], saved[:source]
```

`list` returns the most recently seen contacts first, and 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 `audiences.add_contact`, which the Audiences page covers. `set_audiences` says exactly which lists a contact is in, in one call.

> Addresses are stored lowercased and the gem encodes the one you pass, so `A+B@example.com` reaches the right row. A nil or empty address raises ArgumentError before anything is sent. The address is the identity, so `update` cannot change it: moving a contact is a `delete` and a `create`.

## Parameters: contacts.list

- `limit` (Integer): How many contacts to return per page: a whole number from 1 to 200, defaulting to 50. It is coerced, so a String such as `"100"` read off a query string is fine, and a value outside the range is a 422 rather than a clamped one.
- `cursor` (String): The `next_cursor` from the previous page. Never build one yourself: a cursor naming a contact that no longer exists is a 400 `invalid_cursor`, raised as `OpenEmail::InvalidRequestError`, which means your paging state is stale and the walk should restart without a cursor.
- `source` (String): `manual` for the contacts somebody saved on purpose, `auto` for the ones the app composer recorded. Leave it out for the whole book.
- `q` (String): Searches the name and the address, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead, and the pages that follow keep matching the same way.

## Response: a contact

`contacts.list` returns an `OpenEmail::Page`, so the rows are on `page.items` and the walk follows `page.next_cursor` while `page.has_more?` is true. `list_all` returns every row as one Array, and `iterate` yields them one at a time. `get`, `create`, `update`, `save` and `set_audiences` each return one contact as a Hash with Symbol keys, 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` (String): 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 or nil): The display name, or nil 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` (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. Treat the value as an open String, because the column is free text defaulting to `manual`.
- `notes` (String or nil): Free text somebody wrote about this person, in the app or through `update`, never generated. It is nil when nobody has written any, and `notes: nil` on `update` clears it.
- `lastSeenAt` (String or nil): An ISO 8601 UTC string, bumped every time a member sends to that address from the app composer, not when mail arrives from it, which writes nothing. It is nil 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<Hash>): Only on `get`, `create`, `update`, `save` and `set_audiences`, never on list rows. Every audience the contact is in, the default one included, as a Hash with `id`, `name` and `builtin`. `builtin` is `default` on the audience every contact belongs to and nil on one somebody created, so branch on it rather than on the name, which anybody can change.
- `photoUrl` (String or nil): Where the contact photo is served, or nil when the contact has none. `set_photo` sets it and every upload gets a new URL.

## Setting a contact’s audiences

`set_audiences(email, audienceIds: [...])` says exactly which audiences one contact is in, in one request. The contact joins every audience listed that it is not in yet and leaves every other one, and the call returns the contact after the change, with its `audiences`. It needs `audiences:write`, because it writes memberships rather than the contact, and repeating it changes nothing, so the gem retries it after a network failure.

> The default audience is always kept, so `audienceIds: []` leaves the contact in the default audience alone. It takes up to 100 ids. An id that names no audience in this workspace is a 404 `audience_not_found` and nothing changes, and an address that is not a contact is a 404 `contact_not_found`. Both raise `OpenEmail::NotFoundError`.

## Everyone on the Contacts page

`list_people` lists the people the Contacts page in the app shows: the saved contacts and every address seen in mail, each with `saved`, `threads` and `lastAt`. `list` is the saved contacts alone. It returns an `OpenEmail::PeoplePage`, which adds `seen` to `items`, `has_more?` and `next_cursor`. The addresses seen in mail come only when the key also holds `threads:read`, and `page.seen` says whether they did. `sort:` is `recent`, `name` or `threads`, and `OpenEmail::PEOPLE_SORTS` names them. `q:` searches names, addresses and notes, and `blocked: true` keeps the people the workspace blocklist blocks, whole-domain rules included. `blockedBy` names the rule on every row.

**people.rb**

```
page = client.contacts.list_people(sort: "threads", limit: 50)

page.items.each do |person|
  client.contacts.save(person[:email]) if !person[:saved] && person[:threads].to_i > 5
end

blocked = client.contacts.list_all_people(blocked: true)
puts page.seen, blocked.size
```

> `list_all_people` returns every page as one Array, and `iterate_people` yields each person to a block, or returns an Enumerator without one. Neither reports `seen`, so read one page with `list_people` to learn it. The cursor is opaque, so pass `next_cursor` back as `cursor:` exactly as it came, with the same `sort:`, `q:` and `blocked:`.

## Saving, deleting and photos

`save(email)`, with an optional `name:` and `notes:`, is Add to contacts and Keep in contacts: it saves an address that is not a contact yet, keeps one recorded from a send as saved by hand, and brings back a deleted one. `delete` is Delete: it removes the saved contact and hides the address, so the composer does not record it again, and it takes an address only ever seen in mail too. `wasSaved` in the Hash it returns says which it was. `delete_many` deletes up to 200 in one call.

**photo.rb**

```
client.contacts.save("grace@example.com", name: "Grace Hopper")

contact = client.contacts.set_photo("grace@example.com", File.binread("photo.jpg"), content_type: "image/jpeg")
puts contact[:photoUrl]

client.contacts.set_photo("grace@example.com", Pathname("photo.png"))

client.contacts.remove_photo("grace@example.com")
client.contacts.delete_many(["ada@example.com", "offers@shop.example"])
```

> `set_photo` sends the image bytes as they are: PNG, JPEG, WebP or GIF up to 5 MB, fitted into a 512 pixel square. The bytes are a binary String, an IO or a Pathname. Pass `content_type:`, or bytes that carry their own type: an object that answers `content_type`, such as a Rails upload, or a File or Pathname whose name ends in `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`. Without a type the bytes go as `application/octet-stream`, which the server refuses with a 422 `invalid_image`. `OpenEmail::CONTACT_PHOTO_TYPES` names the four types. The address has to be a saved contact first.

## Blocking

`block(email)` puts the address on the workspace blocklist so mail from it is refused, dropping any plus tag, and `unblock(email)` takes off every rule that blocks it. Both need `settings:write`, because they change the blocklist rather than the contact, and neither needs the address to be a contact.

> When `unblock` lifts a whole-domain rule, `removed` lists it with `list` set to `blockedDomains`, and everybody at that domain is unblocked with it. `OpenEmail::CONTACT_BLOCK_LISTS` names both lists.

## Conversations and activity

`list_threads(email)` pages through the threads the address wrote or was written to, in every folder, and `list_all_threads` and `iterate_threads` walk them. `activity(email)` returns the numbers behind a contact’s Activity tab: received and sent per bucket, threads waiting on your reply, and the median reply time each way. Both need `threads:read`.

**activity.rb**

```
threads = client.contacts.list_threads("ada@example.com", q: "invoice")

activity = client.contacts.activity(
  "ada@example.com",
  minutes: 30 * 24 * 60,
  grain: "day",
  offset_minutes: Time.now.utc_offset / 60
)

puts threads.items.size, activity.dig(:totals, :waiting)
```

> `activity` takes snake_case keywords. `minutes:` sets the window, which is 90 days when left out. `grain:` sets the bucket width: `minute`, `hour` or `day`. `offset_minutes:` sets the minutes east of UTC where days break. `Time.now.utc_offset / 60` is the local offset, and the gem sends it as the API’s `offsetMinutes`.
