Skip to the documentation
Python

openemail.contacts

Every method in this namespace: its signature, its parameters, what it returns and an example.

Methods

The workspace address book: people it has written to, plus anybody you save yourself.

contacts.list()

List the workspace contacts, most recently seen first

Scopescontacts:readPages through results
Signature
def list(    *,    limit: int | None = None,    cursor: str | None = None,    source: ContactSource | None = None,    q: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Page[ContactResource]

Returns one page of the workspace address book, ordered by lastSeenAt with the most recent first and contacts that have never been mailed last. source says where a row came from: manual for a contact somebody saved, in the app or through create, auto for an address recorded when a member sent mail to it from the app composer, and form for an address a sign-up form added. A saved contact stays manual when it is mailed later.

source= narrows the page to one origin, so source='manual' is the contacts somebody saved on purpose and source='auto' the ones the composer recorded. q= searches the name and the address.

This lists saved contacts only. list_people lists everyone the Contacts page in the app shows, the addresses seen in mail included, with thread counts.

Paging is keyset. limit= takes 1 to 200 and defaults to 50, and nextCursor goes back as cursor= while hasMore is True. Never build a cursor yourself.

Contacts belong to the whole workspace rather than to any one address, so a key limited to particular addresses or domains reads and writes the same book as every other key. An app connected by a member who reaches only some addresses is refused with 422 capability_unsupported on addressAllowlist, on every contacts, audiences and broadcasts route.

Parameters

limitint

Rows per page, a whole number from 1 to 200. The server defaults to 50.

cursorstr

The nextCursor from the previous page. Never build one yourself.

sourceContactSource

manual, auto or form. Leave it out for the whole book.

qstr

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.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Page[ContactResource], a dict with items, hasMore and nextCursor. Each contact has email, name, source, notes, attributes, photoUrl, lastSeenAt, createdAt and updatedAt.

Example

from openemail import openemail page = openemail.contacts.list(source='manual', limit=200) for contact in page['items']:    print(contact['name'] or contact['email'], contact['lastSeenAt']) print(page['hasMore'], page['nextCursor'])

Notes

  • Mail sent through this API adds no contacts. Only sends from the app composer record recipients, and create is the way to add one deliberately.

  • Every member of the workspace reads and writes the same book, so a contact saved by one person is visible to the rest.

  • A contact that has never been mailed has lastSeenAt set to None and sorts last, after every contact with a date.

  • Addresses are stored lower cased.

Also available in

API
GET /contacts
TypeScript
contacts.list()
Ruby
contacts.list
PHP
contacts->list
Go
Contacts.List
Java
contacts().list
C#
Contacts.ListAsync
CLI
openemail contacts list

contacts.list_all()

Collect the whole address book into one list

Scopescontacts:readPages through results
Signature
def list_all(    *,    limit: int | None = None,    cursor: str | None = None,    source: ContactSource | None = None,    q: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ContactResource]

Follows nextCursor from page to page and returns every contact in the workspace in one list, most recently seen first and never-mailed contacts last. It takes the same source= and q= filters as list.

Everything is held in memory before the call returns, and an address book grows with every recipient the composer records. Prefer iterate when you can stop early. limit= sets the page size of each request, not the total.

Parameters

limitint

Page size per request, from 1 to 200, defaulting to 50 on the server.

cursorstr

A cursor from an earlier page to start after.

sourceContactSource

manual, auto or form. Leave it out for the whole book.

qstr

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.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ContactResource] holding every contact across all pages.

Example

from openemail import openemail saved = openemail.contacts.list_all(source='manual', limit=200) print(len(saved), 'contacts saved on purpose')

Notes

  • A failure on any page raises out of the whole call, and the contacts already fetched are discarded.

Also available in

API
GET /contacts
TypeScript
contacts.listAll()
Ruby
contacts.list_all
PHP
contacts->listAll
Go
Contacts.ListAll
Java
contacts().listAll
C#
Contacts.ListAllAsync

contacts.iterate()

Stream the address book one contact at a time

Scopescontacts:readPages through results
Signature
def iterate(    *,    limit: int | None = None,    cursor: str | None = None,    source: ContactSource | None = None,    q: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[ContactResource]

Returns a generator that yields contacts one at a time, most recently seen first, and requests the next page only once the current one is used up. Nothing is fetched until the loop starts, and leaving the loop with break stops the requests.

Parameters

limitint

Page size per request, from 1 to 200, defaulting to 50 on the server.

cursorstr

A cursor from an earlier page to start after.

sourceContactSource

manual, auto or form. Leave it out for the whole book.

qstr

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.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[ContactResource], a generator yielding one contact per step.

Example

from openemail import openemail for contact in openemail.contacts.iterate(source='auto'):    if contact['lastSeenAt'] is None:        break     print(contact['email'], contact['lastSeenAt'])

Notes

  • The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in

API
GET /contacts
TypeScript
contacts.iterate()
Ruby
contacts.iterate
PHP
contacts->iterate
Go
Contacts.Iterate
Java
contacts().iterate
C#
Contacts.IterateAsync

contacts.get()

Read one contact by email address

Scopescontacts:read
Signature
def get(    email: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Looks up a contact by address in the workspace address book. The address is trimmed and lower cased before the lookup, so [email protected] finds [email protected], and the SDK URL encodes it for the path.

A 404, which raises OpenEmailApiError with is_not_found, means only that the address is not in the book. It says nothing about whether mail has been exchanged with it, since received mail never creates contacts.

Parameters

emailstrRequired

The contact's address, matched case insensitively.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource with email, name, source, notes, attributes, the custom fields as a dict of keys and text values, photoUrl and lastSeenAt, plus audiences, every audience the contact is in as a dict with id, name and builtin. builtin is 'default' on the audience every contact belongs to and None on one somebody created. A key that also holds forms:read gets signUps, what this address sent through sign-up forms, newest first and at most 20. Each is a dict with id, formId, formName, status ('pending' or 'added'), answers, sourceUrl, createdAt and confirmedAt.

Example

from openemail import OpenEmailApiError, openemail try:    contact = openemail.contacts.get('[email protected]')    print(contact['name'], contact['notes'])except OpenEmailApiError as error:    if error.is_not_found:        print('[email protected] is not in the address book')    else:        raise

Notes

  • name is None for an address recorded automatically without a display name, and notes is free text somebody set in the app or through update.

  • The address is the contact's identity here and on every other contacts route. There is no id in the public contract.

Also available in

API
GET /contacts/{email}
TypeScript
contacts.get()
Ruby
contacts.get
PHP
contacts->get
Go
Contacts.Get
Java
contacts().get
C#
Contacts.GetAsync
CLI
openemail contacts get

contacts.create()

Save a contact in the workspace address book

Scopescontacts:write
Signature
def create(    body: ContactCreate,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Adds one address to the workspace address book and returns the saved row. email is trimmed and lower cased before it is stored, so [email protected] and [email protected] are the same contact.

The contact is saved with source set to manual, the same value a contact typed into the app carries, and it joins the default audience as it is written. Every contact is in that audience for as long as it exists, so there is nothing to add afterwards. Name audiences of your own in audienceIds to put it in them in the same call, or add it later with audiences.add_contact. Sending audienceIds also requires the audiences:write scope, since it writes memberships as well as a contact.

An address can be in the book once. A second create for an address that is already there is refused with 409 contact_exists rather than merged, so a retry cannot quietly overwrite a name somebody edited in the app. Read the existing row with get and change it with update, or use save, which accepts either.

Parameters

body['email']strRequired

The address to save, trimmed and lower cased before it is stored.

body['name']str | None

Display name. Leave it out to save the contact without one.

body['notes']str | None

Free text kept with the contact and shown beside it in the app.

body['attributes']dict[str, str | None]

Custom fields to set, as a dict of keys and text values such as {'plan': 'team'}. A key starts with a letter and holds up to 64 letters, digits and underscores, and a value is text of up to 500 characters, trimmed before it is stored. None or an empty string removes a field, a field you leave out keeps its value, and a contact holds at most 50.

body['audienceIds']list[str]

Up to 25 audience ids to put the new contact in. The default audience is joined whether or not it is named. When this is present the key also needs audiences:write, or the call is refused with 403 before anything is saved.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource with email, name, source as manual, notes, attributes and lastSeenAt, which is None until mail goes to the address from the app composer, plus audiences, every audience the contact is now in, the default one included.

Example

from openemail import openemail contact = openemail.contacts.create(    {        'email': '[email protected]',        'name': 'Grace Hopper',        'notes': 'Met at the compiler workshop',        'attributes': {'plan': 'team'},    }) print(contact['email'], contact['source'], contact['attributes'])

Notes

  • The SDK does not retry a create after a network failure. If you retry by hand after a lost response, a 409 contact_exists means the first attempt landed, and is_conflict on the error says so.

  • Contacts belong to the workspace, so the contact this creates is the one every member and every other key sees.

  • An id in audienceIds that is not an audience of this workspace fails the whole create. Nothing is saved.

  • A custom field that breaks a rule is 422 invalid_contact, with param set to attributes.<key> for a bad key or a value over 500 characters and to attributes for a 51st field. Nothing is written.

Also available in

API
POST /contacts
TypeScript
contacts.create()
Ruby
contacts.create
PHP
contacts->create
Go
Contacts.Create
Java
contacts().create
C#
Contacts.CreateAsync
CLI
openemail contacts create

contacts.update()

Change a contact's name, notes or custom fields

Scopescontacts:write
Signature
def update(    email: str,    patch: ContactPatch,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Changes the fields you send and leaves the rest alone. A key you leave out keeps its stored value, and an explicit None clears it, so {'notes': None} empties the notes while {} changes nothing. attributes is merged key by key: a string sets a custom field, None removes one and the fields you do not name are kept.

The address cannot be changed. It is the contact's identity, its path segment and the unique key of the book, so moving a contact to a new address is a delete and a create, and that new contact starts with no audience membership beyond the default one.

source and lastSeenAt are the server's to set and are not accepted here. A contact recorded automatically stays auto after you give it a name.

Parameters

emailstrRequired

The contact's address, matched case insensitively.

patch['name']str | None

New display name. None clears it.

patch['notes']str | None

New notes. None clears them.

patch['attributes']dict[str, str | None]

Custom fields to set, as a dict of keys and text values such as {'plan': 'team'}. A key starts with a letter and holds up to 64 letters, digits and underscores, and a value is text of up to 500 characters, trimmed before it is stored. None or an empty string removes a field, a field you leave out keeps its value, and a contact holds at most 50.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource as it stands after the change, with audiences alongside it.

Example

from openemail import openemail contact = openemail.contacts.update(    '[email protected]',    {'name': 'Grace Hopper', 'notes': None, 'attributes': {'plan': 'team', 'trial': None}},) print(contact['name'], contact['notes'], contact['attributes'])

Notes

  • The SDK retries this call after a network failure, since the same patch sent twice leaves the same contact.

  • An address that is not in the book is a 404, the same as get.

  • A custom field that breaks a rule is 422 invalid_contact, with param set to attributes.<key> for a bad key or a value over 500 characters and to attributes for a 51st field. Nothing is written.

  • Audience membership is not touched here. Use set_audiences to set the whole list, or audiences.add_contact and audiences.remove_contact for one audience.

Also available in

API
PATCH /contacts/{email}
TypeScript
contacts.update()
Ruby
contacts.update
PHP
contacts->update
Go
Contacts.Update
Java
contacts().update
C#
Contacts.UpdateAsync
CLI
openemail contacts update

contacts.delete()

Delete somebody from the contacts and hide the address

Scopescontacts:write
Signature
def delete(    email: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> DeletedContactResource

Does what Delete does on the Contacts page in the app. A saved contact goes with its notes, its photo and every audience it was in, the default one included. The address is then hidden: list_people leaves it out, and mail sent to it from the app composer no longer records it as a contact. The address can be one that was only ever seen in mail, which is how you take somebody off the people list, and wasSaved says which it was.

Nothing else moves: the mail exchanged with the address stays in the mailbox, and the address can still be written to. There is no undo. create or save afterwards brings the address back as a new contact with no name, no notes and no membership beyond the default audience.

Parameters

emailstrRequired

The contact's address, matched case insensitively.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

DeletedContactResource with object set to 'contact', email, deleted set to True and wasSaved, which is True when the address was a saved contact.

Example

from openemail import openemail removed = openemail.contacts.delete('[email protected]') print(removed['email'], 'was saved' if removed['wasSaved'] else 'was only seen in mail')

Notes

  • An address that is not in the book is no longer a 404: it is hidden and answers with wasSaved set to False. Something that is not an address is a 422 invalid_contact.

  • The SDK does not retry a delete, though a second attempt is harmless: it answers with wasSaved set to False.

  • delete_many deletes up to 200 addresses in one call.

Also available in

API
DELETE /contacts/{email}
TypeScript
contacts.delete()
Ruby
contacts.delete
PHP
contacts->delete
Go
Contacts.Delete
Java
contacts().delete
C#
Contacts.DeleteAsync
CLI
openemail contacts delete

contacts.set_audiences()

Set exactly which audiences a contact is in

Scopesaudiences:write
Signature
def set_audiences(    email: str,    body: ContactAudiencesSet,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Makes the contact's audiences match the list you send, the way the audience picker on a contact does in the app. The contact joins every listed audience it is not in yet and leaves every other one, in one transaction, and memberships it keeps keep their addedAt.

The default audience is always kept, whether or not you name it, so {'audienceIds': []} leaves the contact in the default audience alone. To add or remove one audience without restating the rest, use audiences.add_contact or audiences.remove_contact.

The address is trimmed and lower cased, the SDK URL encodes it for the path, and it has to be a contact already: an address that is not in the book is a 404 contact_not_found on email. Create it with create, which takes audienceIds too. An id that is not an audience of this workspace is a 404 audience_not_found on audienceIds, and nothing is changed.

Parameters

emailstrRequired

The contact's address, matched case insensitively.

body['audienceIds']list[str]Required

Every audience the contact should be in, up to 100 ids. A repeated id counts once. More than 100 is a 422 invalid_parameter on audienceIds, and an empty id is the same error on that entry, such as audienceIds.0.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource as it stands after the change, with audiences, every audience the contact is now in as a dict with id, name and builtin, the default one included.

Example

from openemail import openemail contact = openemail.contacts.set_audiences(    '[email protected]',    {'audienceIds': ['aud_9f2c4b7e1a0d63d84c5f2e7b', 'aud_1c4e7a9b2d0f36e85a7c1b4d']},) print([audience['name'] for audience in contact['audiences']])

Notes

  • audiences:write is the only scope checked, because this writes memberships rather than the contact.

  • Safe to replay: sending the same list again changes nothing. The SDK retries it after a network failure.

  • Read the current list with get first when you mean to add one audience to what the contact already has, or use audiences.add_contact.

Also available in

API
PUT /contacts/{email}/audiences
TypeScript
contacts.setAudiences()
Ruby
contacts.set_audiences
PHP
contacts->setAudiences
Go
Contacts.SetAudiences
Java
contacts().setAudiences
C#
Contacts.SetAudiencesAsync
CLI
openemail contacts set-audiences

contacts.list_people()

List everyone on the Contacts page, saved or seen in mail

Scopescontacts:readPages through results
Signature
def list_people(    *,    limit: int | None = None,    cursor: str | None = None,    q: str | None = None,    email: str | None = None,    sort: PeopleSort | None = None,    blocked: bool | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> PeoplePage

Returns one page of the people the Contacts page in the app lists: the saved contacts, and every address seen in mail as the sender or a recipient of a thread's newest message. Each row says whether it is saved, how many threads it shares with the mailbox and when mail last moved (lastAt). A person seen in mail and saved is one row. list is the saved contacts alone.

The addresses seen in mail are included only when the key also holds threads:read, because they are read out of the mail. Without it the page holds the saved contacts and seen is False. Deleted addresses and the mailbox's own addresses are never listed.

sort= is recent, newest mail first and saved contacts never seen in mail after them, name, by name or else by address, ignoring case, or threads, most threads first. blockedBy on each row names the workspace blocklist rule that blocks it, and blocked=True narrows the page to those rows.

Paging is keyset. limit= takes 1 to 100 and defaults to 25, and nextCursor goes back as cursor=, with the same sort=, q= and blocked=, while hasMore is True.

Parameters

limitint

Rows per page, 1 to 100. The server defaults to 25.

cursorstr

The nextCursor from the previous page. Never build one yourself.

qstr

Searches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.

emailstr

One address only, matched case insensitively: the way to read one person's thread count and last mail.

sortPeopleSort

recent (the default), name or threads.

blockedbool

True for only the people the workspace blocklist blocks, whole-domain rules included.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

PeoplePage, a dict with items, hasMore, nextCursor and seen. Each PersonResource has email, displayEmail, name, saved, source, notes, attributes, which is empty for an address only seen in mail, photoUrl, threads, lastAt, createdAt, updatedAt and blockedBy.

Example

from openemail import openemail page = openemail.contacts.list_people(sort='threads', limit=50) for person in page['items']:    kind = 'saved' if person['saved'] else 'seen in mail'    print(person['name'] or person['email'], person['threads'], kind) print('Addresses seen in mail included:', page['seen'])

Notes

  • email is lower cased and is what every other contacts method takes. displayEmail keeps the capitals the newest message wrote.

  • A malformed or stale cursor is a 400 invalid_cursor. Start again without one.

  • A key limited to particular addresses or domains gets the saved contacts alone, with seen set to False, even when it holds threads:read, because the addresses seen in mail would be read from the mail of every address in the workspace.

Also available in

API
GET /contacts/people
TypeScript
contacts.listPeople()
Ruby
contacts.list_people
PHP
contacts->listPeople
Go
Contacts.ListPeople
Java
contacts().listPeople
C#
Contacts.ListPeopleAsync
CLI
openemail contacts list-people

contacts.list_all_people()

Collect everyone on the Contacts page into one list

Scopescontacts:readPages through results
Signature
def list_all_people(    *,    limit: int | None = None,    cursor: str | None = None,    q: str | None = None,    email: str | None = None,    sort: PeopleSort | None = None,    blocked: bool | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[PersonResource]

Follows nextCursor from page to page and returns every person list_people would list in one list, in the same order and with the same filters. It is how you count the people a blocklist blocks: len(openemail.contacts.list_all_people(blocked=True)).

Everything is held in memory before the call returns, and a busy mailbox has seen a great many addresses. Prefer iterate_people when you can stop early.

Parameters

limitint

Page size per request, from 1 to 100, defaulting to 25 on the server.

cursorstr

A cursor from an earlier page to start after.

qstr

Searches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.

emailstr

One address only, matched case insensitively: the way to read one person's thread count and last mail.

sortPeopleSort

recent (the default), name or threads.

blockedbool

True for only the people the workspace blocklist blocks, whole-domain rules included.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[PersonResource] holding every person across all pages.

Example

from openemail import openemail blocked = openemail.contacts.list_all_people(blocked=True, limit=100) print(len(blocked), 'people are blocked')

Notes

  • A failure on any page raises out of the whole call, and the people already fetched are discarded.

  • seen is not reported here. Read one page with list_people to learn whether the key reads the people seen in mail.

Also available in

API
GET /contacts/people
TypeScript
contacts.listAllPeople()
Ruby
contacts.list_all_people
PHP
contacts->listAllPeople
Go
Contacts.ListAllPeople
Java
contacts().listAllPeople
C#
Contacts.ListAllPeopleAsync

contacts.iterate_people()

Stream everyone on the Contacts page one person at a time

Scopescontacts:readPages through results
Signature
def iterate_people(    *,    limit: int | None = None,    cursor: str | None = None,    q: str | None = None,    email: str | None = None,    sort: PeopleSort | None = None,    blocked: bool | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[PersonResource]

Returns a generator that yields the people list_people lists, one at a time, and requests the next page only once the current one is used up. Nothing is fetched until the loop starts, and leaving the loop with break stops the requests.

Parameters

limitint

Page size per request, from 1 to 100, defaulting to 25 on the server.

cursorstr

A cursor from an earlier page to start after.

qstr

Searches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.

emailstr

One address only, matched case insensitively: the way to read one person's thread count and last mail.

sortPeopleSort

recent (the default), name or threads.

blockedbool

True for only the people the workspace blocklist blocks, whole-domain rules included.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[PersonResource], a generator yielding one person per step.

Example

from openemail import openemail for person in openemail.contacts.iterate_people(sort='recent'):    if not person['saved'] and (person['threads'] or 0) > 5:        openemail.contacts.save(person['email'])        print('Saved', person['email'])

Notes

  • The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in

API
GET /contacts/people
TypeScript
contacts.iteratePeople()
Ruby
contacts.iterate_people
PHP
contacts->iteratePeople
Go
Contacts.IteratePeople
Java
contacts().iteratePeople
C#
Contacts.IteratePeopleAsync

contacts.save()

Save an address as a contact, or keep one that was recorded

Scopescontacts:write
Signature
def save(    email: str,    body: ContactSave | None = None,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Does what Add to contacts and Keep in contacts do in the app, and is safe to call whatever state the address is in. An address that is not a contact yet becomes one with source manual. A contact recorded from a send becomes manual. A contact already saved keeps what it has. A deleted address is brought back.

body['name'] replaces the stored name and leaving it out keeps it. body['notes'] replaces the stored notes and None clears them. body['attributes'] sets the custom fields it names and keeps the rest. Unlike create, an address already in the book is not an error, and unlike update, an address not in the book is not one either. body is optional, so save(email) keeps whatever the contact already has.

Parameters

emailstrRequired

The address, trimmed and lower cased on the server.

body['name']str

Up to 200 characters. Left out, the stored name is kept.

body['notes']str | None

Up to 5,000 characters. None clears the notes, and left out, they are kept.

body['attributes']dict[str, str | None]

Custom fields to set, as a dict of keys and text values such as {'plan': 'team'}. A key starts with a letter and holds up to 64 letters, digits and underscores, and a value is text of up to 500 characters, trimmed before it is stored. None or an empty string removes a field, a field you leave out keeps its value, and a contact holds at most 50.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource, the contact as it stands after the save, with audiences.

Example

from openemail import openemail contact = openemail.contacts.save('[email protected]', {'name': 'Grace Hopper'}) print(contact['email'], contact['source'])

Notes

  • The server answers 201 for a new contact and 200 for one that was there. The method returns the contact either way, so compare createdAt with updatedAt if you need to tell them apart.

  • Safe to replay, so the SDK retries it after a network failure.

  • A new contact joins the default audience, as it does from create.

Also available in

API
PUT /contacts/{email}
TypeScript
contacts.save()
Ruby
contacts.save
PHP
contacts->save
Go
Contacts.Save
Java
contacts().save
C#
Contacts.SaveAsync
CLI
openemail contacts save

contacts.delete_many()

Delete up to 200 contacts in one call

Scopescontacts:write
Signature
def delete_many(    emails: Sequence[str],    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactBatchDeleteResource

Deletes every address in emails the way delete deletes one: a saved contact goes with its notes, its photo and every audience membership, and every address is hidden, so mail sent to it from the app composer does not record it again. Addresses that were only seen in mail are hidden too.

An entry that is not an address comes back in invalid and the rest still go through. A repeated address counts once. There is no undo.

Parameters

emailsSequence[str]Required

A list of 1 to 200 addresses, matched case insensitively. More than 200, or none, is a 422 on emails.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactBatchDeleteResource with object set to 'contact_batch_delete', deleted, saved and invalid. deleted counts the addresses deleted and hidden, saved how many of them were saved contacts, and invalid lists the entries that were not addresses.

Example

from openemail import openemail result = openemail.contacts.delete_many(['[email protected]', '[email protected]']) print(result['deleted'], 'deleted,', result['saved'], 'of them saved contacts')

Notes

  • Safe to replay: deleting an address twice leaves it deleted and hidden, so the SDK retries it after a network failure.

  • No mail is deleted, and nobody is unsubscribed from anything.

Also available in

API
POST /contacts/batch-delete
TypeScript
contacts.deleteMany()
Ruby
contacts.delete_many
PHP
contacts->deleteMany
Go
Contacts.DeleteMany
Java
contacts().deleteMany
C#
Contacts.DeleteManyAsync
CLI
openemail contacts delete-many

contacts.set_photo()

Upload the photo shown for a contact

Scopescontacts:write
Signature
def set_photo(    email: str,    data: RawBody,    *,    content_type: ContactPhotoType | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Sends the image bytes as the request body, replacing any photo the contact had. PNG, JPEG, WebP and GIF are accepted, up to 5 MB. The server fits the image into a 512 pixel square, stores it as WebP, keeps the first frame of an animation, and answers with the contact and its new photoUrl.

data is bytes, a bytearray or a memoryview. The type is read from content_type=, and without it the bytes go as application/octet-stream, which the server refuses with 422 invalid_image, so always pass it.

The address has to be a saved contact already: save it first.

Parameters

emailstrRequired

The contact's address, matched case insensitively.

dataRawBodyRequired

The image as bytes, a bytearray or a memoryview.

content_typeContactPhotoType

image/png, image/jpeg, image/webp or image/gif. Left out, the upload is refused with 422 invalid_image.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource with the new photoUrl.

Example

from pathlib import Path from openemail import openemail contact = openemail.contacts.set_photo(    '[email protected]', Path('photo.jpg').read_bytes(), content_type='image/jpeg') print(contact['photoUrl'])

Notes

  • An address that is not a saved contact is a 404 contact_not_found.

  • A busy image service answers 503 image_busy, which the SDK retries like any other 503.

  • Every upload gets a new URL, so a cached old photo never shows under the new one.

Also available in

API
PUT /contacts/{email}/photo
TypeScript
contacts.setPhoto()
Ruby
contacts.set_photo
PHP
contacts->setPhoto
Go
Contacts.SetPhoto
Java
contacts().setPhoto
C#
Contacts.SetPhotoAsync
CLI
openemail contacts set-photo

contacts.remove_photo()

Remove a contact photo

Scopescontacts:write
Signature
def remove_photo(    email: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactDetailResource

Takes the photo off the contact and deletes the stored image, the way Remove does on a contact in the app. The call returns the contact with photoUrl set to None. Removing a photo from a contact that has none changes nothing.

Parameters

emailstrRequired

The contact's address, matched case insensitively.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactDetailResource with photoUrl set to None.

Example

from openemail import openemail contact = openemail.contacts.remove_photo('[email protected]') print(contact['email'], contact['photoUrl'])

Notes

  • An address that is not a saved contact is a 404 contact_not_found.

  • Safe to replay, so the SDK retries it after a network failure.

Also available in

API
DELETE /contacts/{email}/photo
TypeScript
contacts.removePhoto()
Ruby
contacts.remove_photo
PHP
contacts->removePhoto
Go
Contacts.RemovePhoto
Java
contacts().removePhoto
C#
Contacts.RemovePhotoAsync
CLI
openemail contacts remove-photo

contacts.block()

Block an address

Scopessettings:write
Signature
def block(    email: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactBlockResource

Puts the address on the workspace blocklist, the same list settings.update edits as blockedSenders, so mail from it is refused from then on. This is Block on a contact in the app. A plus tag is dropped: blocking [email protected] blocks [email protected], and every tag of it.

When a rule already blocks the address, a whole-domain rule included, nothing is added: created is False and blockedBy names that rule. The address does not have to be a contact.

Parameters

emailstrRequired

The address to block.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactBlockResource with object set to 'contact_block', email, blocked set to True, blockedBy, the rule that blocks it as a dict with rule and list, and created. Both are optional keys of the type, which unblock shares, so read them with .get().

Example

from openemail import openemail result = openemail.contacts.block('[email protected]')rule = result.get('blockedBy') if result.get('created'):    print('Blocked', result['email'])elif rule is not None:    print('Already blocked by', rule['rule'], 'in', rule['list'])

Notes

  • It needs settings:write rather than contacts:write, because it writes the blocklist rather than the contact.

  • Safe to replay, so the SDK retries it after a network failure.

  • An address with fewer than two letters or numbers is refused with 422 blocklist_entry_too_broad.

  • A key limited to particular addresses or domains gets 422 capability_unsupported on addressAllowlist, because the blocklist belongs to the whole workspace and filters the mail of every address in it.

Also available in

API
POST /contacts/{email}/block
TypeScript
contacts.block()
Ruby
contacts.block
PHP
contacts->block
Go
Contacts.Block
Java
contacts().block
C#
Contacts.BlockAsync
CLI
openemail contacts block

contacts.unblock()

Unblock an address

Scopessettings:write
Signature
def unblock(    email: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactBlockResource

Takes every workspace blocklist rule that blocks the address off the list and lists them in removed. This is Unblock on a contact in the app.

When one of them is a whole-domain rule, in blockedDomains, everybody at that domain is unblocked with it, so check removed when that matters. Rules set for one address or one domain in the settings are not touched. An address that no rule blocks answers with removed empty.

Parameters

emailstrRequired

The address to unblock.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactBlockResource with object set to 'contact_block', email, blocked set to False and removed, a list of dicts with rule and list. It is an optional key of the type, which block shares, so read it with .get().

Example

from openemail import openemail result = openemail.contacts.unblock('[email protected]') for removed in result.get('removed', []):    print('Removed', removed['rule'], 'from', removed['list'])

Notes

  • Safe to replay, so the SDK retries it after a network failure.

  • A key limited to particular addresses or domains gets 422 capability_unsupported on addressAllowlist, because the blocklist belongs to the whole workspace and filters the mail of every address in it.

Also available in

API
DELETE /contacts/{email}/block
TypeScript
contacts.unblock()
Ruby
contacts.unblock
PHP
contacts->unblock
Go
Contacts.Unblock
Java
contacts().unblock
C#
Contacts.UnblockAsync
CLI
openemail contacts unblock

contacts.list_threads()

List the conversations with one person

Scopesthreads:readPages through results
Signature
def list_threads(    email: str,    *,    limit: int | None = None,    cursor: str | None = None,    q: str | None = None,    sort: ContactThreadSort | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Page[ContactThreadResource]

Returns one page of the threads the address wrote or was written to, in every folder: the Mail tab on a contact in the app. Each row is a summary, subject, from, receivedAt, messageCount, hasUnread and labels, and threads.get reads the messages behind its id.

limit= takes 1 to 100 and defaults to 25. Pass nextCursor back as cursor=, with the same q= and sort=, while hasMore is True.

Parameters

emailstrRequired

The address. It does not have to be a saved contact.

limitint

Threads per page, 1 to 100. The server defaults to 25.

cursorstr

The nextCursor from the previous page. Never build one yourself.

qstr

Searches inside those threads, with the mailbox search syntax, up to 200 characters.

sortContactThreadSort

newest (the default), oldest, sender or subject.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Page[ContactThreadResource], a dict with items, hasMore and nextCursor.

Example

from openemail import openemail page = openemail.contacts.list_threads('[email protected]', q='invoice') for thread in page['items']:    print(thread['receivedAt'], thread['subject'], thread['messageCount'])

Notes

  • It needs threads:read, because it reads mail rather than the contact.

  • A key limited to particular addresses or domains gets 422 capability_unsupported on addressAllowlist, because a contact's threads and activity are read from the mail of every address in the workspace.

Also available in

API
GET /contacts/{email}/threads
TypeScript
contacts.listThreads()
Ruby
contacts.list_threads
PHP
contacts->listThreads
Go
Contacts.ListThreads
Java
contacts().listThreads
C#
Contacts.ListThreadsAsync
CLI
openemail contacts list-threads

contacts.list_all_threads()

Collect every conversation with one person into one list

Scopesthreads:readPages through results
Signature
def list_all_threads(    email: str,    *,    limit: int | None = None,    cursor: str | None = None,    q: str | None = None,    sort: ContactThreadSort | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ContactThreadResource]

Follows nextCursor from page to page and returns every thread list_threads would list for the address in one list, in the same order.

Everything is held in memory before the call returns. Prefer iterate_threads when you can stop early.

Parameters

emailstrRequired

The address. It does not have to be a saved contact.

limitint

Page size per request, from 1 to 100, defaulting to 25 on the server.

cursorstr

A cursor from an earlier page to start after.

qstr

Searches inside those threads, with the mailbox search syntax, up to 200 characters.

sortContactThreadSort

newest (the default), oldest, sender or subject.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ContactThreadResource] holding every thread across all pages.

Example

from openemail import openemail threads = openemail.contacts.list_all_threads('[email protected]') print(len(threads), 'conversations with Ada')

Notes

  • A failure on any page raises out of the whole call, and the threads already fetched are discarded.

Also available in

API
GET /contacts/{email}/threads
TypeScript
contacts.listAllThreads()
Ruby
contacts.list_all_threads
PHP
contacts->listAllThreads
Go
Contacts.ListAllThreads
Java
contacts().listAllThreads
C#
Contacts.ListAllThreadsAsync

contacts.iterate_threads()

Stream the conversations with one person one thread at a time

Scopesthreads:readPages through results
Signature
def iterate_threads(    email: str,    *,    limit: int | None = None,    cursor: str | None = None,    q: str | None = None,    sort: ContactThreadSort | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[ContactThreadResource]

Returns a generator that yields the threads list_threads lists for the address, one at a time, and requests the next page only once the current one is used up. Nothing is fetched until the loop starts, and leaving the loop with break stops the requests.

Parameters

emailstrRequired

The address. It does not have to be a saved contact.

limitint

Page size per request, from 1 to 100, defaulting to 25 on the server.

cursorstr

A cursor from an earlier page to start after.

qstr

Searches inside those threads, with the mailbox search syntax, up to 200 characters.

sortContactThreadSort

newest (the default), oldest, sender or subject.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[ContactThreadResource], a generator yielding one thread per step.

Example

from openemail import openemail for thread in openemail.contacts.iterate_threads('[email protected]'):    if thread['hasUnread']:        print('Unread:', thread['subject'])

Notes

  • The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in

API
GET /contacts/{email}/threads
TypeScript
contacts.iterateThreads()
Ruby
contacts.iterate_threads
PHP
contacts->iterateThreads
Go
Contacts.IterateThreads
Java
contacts().iterateThreads
C#
Contacts.IterateThreadsAsync

contacts.activity()

Read how mail with one person has gone over a window

Scopesthreads:read
Signature
def activity(    email: str,    *,    minutes: int | None = None,    grain: TrackingGrain | None = None,    offset_minutes: int | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> ContactActivityResource

Returns the numbers behind the Activity tab on a contact in the app, over a window that ends now: messages received from the address and sent to it per bucket, the threads that moved, the threads whose newest message is theirs and so waits on a reply from the mailbox, when each side last wrote, and the median time each side takes to answer, in milliseconds.

minutes= sets how far back the window reaches, 90 days by default. grain= sets the bucket width and the key shape, YYYY-MM-DD, YYYY-MM-DDTHH or YYYY-MM-DDTHH:MM, and offset_minutes= shifts the boundaries so days break where the reader's day does. buckets is sparse and oldest first. Mail in the bin or in spam is left out.

Parameters

emailstrRequired

The address. It does not have to be a saved contact.

minutesint

Window length in minutes, from 1 to about 20 years. The server defaults to 90 days.

grainTrackingGrain

Bucket width: minute, hour or day, defaulting to day.

offset_minutesint

Minutes east of UTC to cut the buckets in, from -840 to 840, defaulting to 0. For the machine's own zone, pass datetime.now().astimezone().utcoffset() converted to whole minutes.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactActivityResource with object set to 'contact_activity', email, since, until, grain, buckets, totals and replyTime. Each bucket has bucket, received and sent. totals has received, sent, threads, waiting, lastReceivedAt and lastSentAt, and replyTime has yours and theirs, each None when there is no reply to measure.

Example

from openemail import openemail activity = openemail.contacts.activity('[email protected]', minutes=30 * 24 * 60, grain='day')totals = activity['totals'] print(totals['received'], 'in,', totals['sent'], 'out,', totals['waiting'], 'waiting on you')

Notes

  • It needs threads:read, because it reads mail rather than the contact.

  • Read only, so the SDK retries it after a network failure like any other read.

  • A key limited to particular addresses or domains gets 422 capability_unsupported on addressAllowlist, because a contact's threads and activity are read from the mail of every address in the workspace.

Also available in

API
GET /contacts/{email}/activity
TypeScript
contacts.activity()
Ruby
contacts.activity
PHP
contacts->activity
Go
Contacts.Activity
Java
contacts().activity
C#
Contacts.ActivityAsync
CLI
openemail contacts activity

contacts.list_events()

List the events recorded for one contact

Scopescontacts:readPages through results
Signature
def list_events(    email: str,    *,    limit: int | None = None,    cursor: str | None = None,    name: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Page[ContactEventResource]

Returns one page of the events events.send recorded for a contact in the last 90 days, the most recent first by when they happened. Each has its name, properties, occurredAt and mode, which is test for an event a test key sent.

limit= takes 1 to 200 and defaults to 50. Pass nextCursor back as cursor=, with the same name=, while hasMore is True. list_all_events and iterate_events do that walk for you.

Parameters

emailstrRequired

The contact, by email address, compared without case. The contactId an event or an enrollment carries works here too.

limitint

Events per page, a whole number from 1 to 200. The server defaults to 50.

cursorstr

The nextCursor from the previous page, passed back exactly as it came. One that names no event of this contact is a 400 invalid_cursor.

namestr

Only events with exactly this name.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Page[ContactEventResource], a dict with items, hasMore and nextCursor. Each item has id, contactId, email, name, properties, occurredAt, mode and createdAt.

Example

from openemail import openemail page = openemail.contacts.list_events('[email protected]', name='order.placed') for event in page['items']:    print(event['occurredAt'], event['name'], event['properties'])

Notes

  • An address or id nobody you can reach has is 404 contact_not_found. An app a member connected reads only the contacts that member added.

  • Read only, so the SDK retries it after a network failure like any other read.

Also available in

API
GET /contacts/{email}/events
TypeScript
contacts.listEvents()
Ruby
contacts.list_events
PHP
contacts->listEvents
Go
Contacts.ListEvents
Java
contacts().listEvents
C#
Contacts.ListEventsAsync
CLI
openemail contacts list-events

contacts.list_all_events()

Collect every event of one contact into one list

Scopescontacts:readPages through results
Signature
def list_all_events(    email: str,    *,    limit: int | None = None,    cursor: str | None = None,    name: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ContactEventResource]

Follows nextCursor from page to page and returns every event recorded for the contact in the last 90 days as one list, the most recent first, in the shape list_events returns. limit= sets the page size of each request, not the total.

Parameters

emailstrRequired

The contact, by email address, compared without case. The contactId an event or an enrollment carries works here too.

limitint

Page size per request, from 1 to 200, defaulting to 50 on the server.

cursorstr

A nextCursor from an earlier page to start after.

namestr

Only events with exactly this name.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ContactEventResource] holding every matching event across all pages.

Example

from openemail import openemail events = openemail.contacts.list_all_events('[email protected]') names = {event['name'] for event in events} print(len(events), sorted(names))

Notes

  • A failure on any page raises, and the events already fetched are discarded.

Also available in

API
GET /contacts/{email}/events
TypeScript
contacts.listAllEvents()
Ruby
contacts.list_all_events
PHP
contacts->listAllEvents
Go
Contacts.ListAllEvents
Java
contacts().listAllEvents
C#
Contacts.ListAllEventsAsync

contacts.iterate_events()

Stream the events of one contact one at a time

Scopescontacts:readPages through results
Signature
def iterate_events(    email: str,    *,    limit: int | None = None,    cursor: str | None = None,    name: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[ContactEventResource]

Returns a generator that yields the events of a contact one by one, the most recent first, and requests the next page only once the current one is used up. Breaking out of the loop stops the requests.

Parameters

emailstrRequired

The contact, by email address, compared without case. The contactId an event or an enrollment carries works here too.

limitint

Page size per request, from 1 to 200, defaulting to 50 on the server.

cursorstr

A nextCursor from an earlier page to start after.

namestr

Only events with exactly this name.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[ContactEventResource], a generator yielding one event per step.

Example

from openemail import openemail for event in openemail.contacts.iterate_events('[email protected]'):    if event['name'] == 'plan.cancelled':        print('cancelled on', event['occurredAt'])         break

Notes

  • The generator is lazy, so an abandoned loop costs only the pages it read.

Also available in

API
GET /contacts/{email}/events
TypeScript
contacts.iterateEvents()
Ruby
contacts.iterate_events
PHP
contacts->iterateEvents
Go
Contacts.IterateEvents
Java
contacts().iterateEvents
C#
Contacts.IterateEventsAsync

contacts.list_cards()

List the address book as contact cards

Scopescontacts:readPages through results
Signature
def list_cards(    *,    limit: int | None = None,    cursor: str | None = None,    email: str | None = None,    without_email: bool | None = None,    kind: ContactCardKind | None = None,    group_id: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Page[ContactCardResource]

Returns one page of contact cards, newest first. A card is a saved contact with everything an address book keeps for it: phone numbers, other email addresses, postal addresses, websites, organisation, department, job title, birthday, nickname and the name in parts. Cards with no email address, such as a plumber saved on a phone, are listed too. It is the address book phones and computers sync over CardDAV.

email= finds the card of one saved contact, and without_email=True lists only the cards with no address. An address that was only ever mailed, and never saved, has no card.

Paging is keyset. limit= takes 1 to 200 and defaults to 50, and nextCursor goes back as cursor= while hasMore is True. Never build a cursor yourself.

Parameters

limitint

Cards per page, a whole number from 1 to 200. The server defaults to 50.

cursorstr

The nextCursor from the previous page. Never build one yourself.

emailstr

Only the card of the saved contact with this address.

without_emailbool

True lists only the cards that have no email address.

kindContactCardKind

'group' lists only the groups, 'individual' only the people and companies, one of CONTACT_CARD_KINDS.

group_idstr

Only the cards in the group with this id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Page[ContactCardResource], a dict with items, hasMore and nextCursor. Each card has id, uid, email, name, nameParts, nickname, organization, department, title, birthday, notes, the lists emails, phones, addresses and urls, photoUrl, createdAt and updatedAt.

Example

from openemail import openemail page = openemail.contacts.list_cards(without_email=True) for card in page['items']:    print(card['name'], [phone['value'] for phone in card['phones']]) print(page['hasMore'], page['nextCursor'])

Notes

  • Every member reads the same address book through a key, since a key acts for the workspace owner.

  • A cursor that names no card is a 400 invalid_cursor. Start again without one.

Also available in

API
GET /contacts/cards
TypeScript
contacts.listCards()
Ruby
contacts.list_cards
PHP
contacts->listCards
Go
Contacts.ListCards
Java
contacts().listCards
C#
Contacts.ListCardsAsync
CLI
openemail contacts list-cards

contacts.list_all_cards()

Collect every contact card into one list

Scopescontacts:readPages through results
Signature
def list_all_cards(    *,    limit: int | None = None,    cursor: str | None = None,    email: str | None = None,    without_email: bool | None = None,    kind: ContactCardKind | None = None,    group_id: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ContactCardResource]

Follows nextCursor from page to page and returns every card in the address book in one list, newest first. It takes the same email= and without_email= filters as list_cards.

Everything is held in memory before the call returns. Prefer iterate_cards when you can stop early. limit= sets the page size of each request, not the total.

Parameters

limitint

Page size per request, from 1 to 200, defaulting to 50 on the server.

cursorstr

A cursor from an earlier page to start after.

emailstr

Only the card of the saved contact with this address.

without_emailbool

True lists only the cards that have no email address.

kindContactCardKind

'group' lists only the groups, 'individual' only the people and companies, one of CONTACT_CARD_KINDS.

group_idstr

Only the cards in the group with this id.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ContactCardResource] holding every card across all pages.

Example

from openemail import openemail cards = openemail.contacts.list_all_cards() print(len(cards), 'cards in the address book')

Notes

  • A failure on any page raises out of the whole call, and the cards already fetched are discarded.

Also available in

API
GET /contacts/cards
TypeScript
contacts.listAllCards()
Ruby
contacts.list_all_cards
PHP
contacts->listAllCards
Go
Contacts.ListAllCards
Java
contacts().listAllCards
C#
Contacts.ListAllCardsAsync

contacts.iterate_cards()

Stream the address book one card at a time

Scopescontacts:readPages through results
Signature
def iterate_cards(    *,    limit: int | None = None,    cursor: str | None = None,    email: str | None = None,    without_email: bool | None = None,    kind: ContactCardKind | None = None,    group_id: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[ContactCardResource]

Returns a generator that yields cards one at a time, newest first, and requests the next page only once the current one is used up. Nothing is fetched until the loop starts, and leaving the loop with break stops the requests. It takes the same email= and without_email= filters as list_cards.

Parameters

limitint

Page size per request, from 1 to 200, defaulting to 50 on the server.

cursorstr

A cursor from an earlier page to start after.

emailstr

Only the card of the saved contact with this address.

without_emailbool

True lists only the cards that have no email address.

kindContactCardKind

'group' lists only the groups, 'individual' only the people and companies, one of CONTACT_CARD_KINDS.

group_idstr

Only the cards in the group with this id.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[ContactCardResource], a generator yielding one card per step.

Example

from openemail import openemail for card in openemail.contacts.iterate_cards():    if card['birthday']:        print(card['name'], card['birthday'])

Notes

  • The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in

API
GET /contacts/cards
TypeScript
contacts.iterateCards()
Ruby
contacts.iterate_cards
PHP
contacts->iterateCards
Go
Contacts.IterateCards
Java
contacts().iterateCards
C#
Contacts.IterateCardsAsync

contacts.get_card()

Read one contact card

Scopescontacts:read
Signature
def get_card(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactCardResource

Returns one card with everything it holds. To find the card of a saved contact by its address, pass email= to list_cards.

Parameters

idstrRequired

The card id, ccd_ followed by 24 characters, from list_cards or get_card.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactCardResource with id, uid, the vCard UID that phones and computers know the card by, email, which is None on a card with no address, name, nameParts, nickname, organization, department, title, birthday, notes, photoUrl, createdAt and updatedAt. emails, phones and urls are lists of dicts with value, label and customLabel, and addresses is a list of dicts with label, customLabel, street, locality, region, postalCode and country.

Example

from openemail import openemail card = openemail.contacts.get_card('ccd_0123456789abcdef01234567') print(card['name'], card['organization'], [phone['value'] for phone in card['phones']])

Notes

Also available in

API
GET /contacts/cards/{id}
TypeScript
contacts.getCard()
Ruby
contacts.get_card
PHP
contacts->getCard
Go
Contacts.GetCard
Java
contacts().getCard
C#
Contacts.GetCardAsync
CLI
openemail contacts get-card

contacts.create_card()

Add a card to the address book

Scopescontacts:write
Signature
def create_card(    body: ContactCardCreate,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactCardResource

Adds a contact card. With email the card is a saved contact as well, the same one create makes, and an address the workspace already knows from mail becomes saved. Without one it is a card on its own, kept for its name, phone numbers and the rest. A card needs at least a name, an email address, a phone number or an organisation.

Each entry in emails, phones and urls is a dict with value, and optionally label, one of CONTACT_CARD_LABELS, and customLabel, a label of your own.

Phones and computers that sync the address book over CardDAV get the new card on their next sync.

Parameters

body['email']str | None

The address OpenEmail knows the contact by. With it the card is a saved contact as well. A saved contact keeps its address, so put any other address in emails.

body['name']str | None

The full name the address book shows, up to 200 characters.

body['nameParts']ContactCardNameInput | None

The name in parts, a dict with prefix, given, middle, family and suffix, the way phones keep it.

body['nickname']str | None

A nickname.

body['organization']str | None

The company or organisation.

body['department']str | None

The department within the organisation.

body['title']str | None

The job title.

body['birthday']str | None

YYYY-MM-DD, or --MM-DD when the year is not known.

body['notes']str | None

Free-form notes. On a saved contact these are the notes the contact page shows.

body['emails']list[ContactCardValueInput]

Other email addresses besides email, each a dict with value, and optionally label and customLabel.

body['phones']list[ContactCardValueInput]

Phone numbers, each a dict with value, and optionally label and customLabel.

body['addresses']list[ContactCardAddressInput]

Postal addresses, each a dict with any of label, customLabel, street, locality, region, postalCode and country.

body['urls']list[ContactCardValueInput]

Websites and profile links, each a dict with value, and optionally label and customLabel.

body['kind']ContactCardKind

'group' makes a group, the kind phones keep, which needs name and takes members instead of an address. 'individual' when left out.

body['members']list[str]

For a group, the ids of the cards in it, up to 2,000. Groups cannot hold other groups.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactCardResource for the new card.

Example

from openemail import openemail card = openemail.contacts.create_card(    {        'email': '[email protected]',        'name': 'Ada Lovelace',        'organization': 'Analytical Engines',        'phones': [{'value': '+44 20 7946 0000', 'label': 'work'}],        'birthday': '1815-12-10',    }) print(card['id'], card['email'])

Notes

  • The SDK does not retry it after a network failure, because a second call would make a second card.

  • An address that already belongs to another card is a 409 contact_card_email_taken, and a card with nothing to keep a 422 invalid_contact_card.

  • A workspace that already keeps as many cards as it may gets a 409 contact_card_limit. Delete some before you add more.

Also available in

API
POST /contacts/cards
TypeScript
contacts.createCard()
Ruby
contacts.create_card
PHP
contacts->createCard
Go
Contacts.CreateCard
Java
contacts().createCard
C#
Contacts.CreateCardAsync
CLI
openemail contacts create-card

contacts.update_card()

Change a contact card

Scopescontacts:write
Signature
def update_card(    id: str,    patch: ContactCardPatch,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactCardResource

Changes the fields you send and leaves the rest alone. None clears a field, and a list you send (emails, phones, addresses or urls) replaces the whole list, so read the card first when you mean to add one entry.

On a saved contact, name and notes are the contact's own, so the contact page shows the change. email can only be given to a card that has none, which makes it a saved contact. A saved contact keeps its address: add another address to emails instead.

Phones and computers that sync the address book over CardDAV get the change on their next sync.

Parameters

idstrRequired

The card id, ccd_ followed by 24 characters, from list_cards or get_card.

patch['email']str | None

The address OpenEmail knows the contact by. Given to a card that has none, it makes the card a saved contact. A saved contact keeps its address, so put any other address in emails.

patch['name']str | None

The full name the address book shows, up to 200 characters.

patch['nameParts']ContactCardNameInput | None

The name in parts, a dict with prefix, given, middle, family and suffix, the way phones keep it.

patch['nickname']str | None

A nickname.

patch['organization']str | None

The company or organisation.

patch['department']str | None

The department within the organisation.

patch['title']str | None

The job title.

patch['birthday']str | None

YYYY-MM-DD, or --MM-DD when the year is not known.

patch['notes']str | None

Free-form notes. On a saved contact these are the notes the contact page shows.

patch['emails']list[ContactCardValueInput]

Other email addresses besides email, each a dict with value, and optionally label and customLabel. The list replaces the one the card has.

patch['phones']list[ContactCardValueInput]

Phone numbers, each a dict with value, and optionally label and customLabel. The list replaces the one the card has.

patch['addresses']list[ContactCardAddressInput]

Postal addresses, each a dict with any of label, customLabel, street, locality, region, postalCode and country. The list replaces the one the card has.

patch['urls']list[ContactCardValueInput]

Websites and profile links, each a dict with value, and optionally label and customLabel. The list replaces the one the card has.

patch['kind']ContactCardKind

What the card already is. A person cannot become a group or a group a person, so any other value is a 422 invalid_contact_card.

patch['members']list[str]

For a group, the ids of the cards in it. Given, it replaces the whole list, so send the ids already in it with the ones you add.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactCardResource as it stands after the change.

Example

from openemail import openemail card = openemail.contacts.update_card(    'ccd_0123456789abcdef01234567',    {        'title': 'Head of Research',        'nickname': None,        'phones': [            {'value': '+44 20 7946 0000', 'label': 'work'},            {'value': '+1 555 0100', 'label': 'mobile'},        ],    },) print(card['title'], [phone['value'] for phone in card['phones']])

Notes

Also available in

API
PATCH /contacts/cards/{id}
TypeScript
contacts.updateCard()
Ruby
contacts.update_card
PHP
contacts->updateCard
Go
Contacts.UpdateCard
Java
contacts().updateCard
C#
Contacts.UpdateCardAsync
CLI
openemail contacts update-card

contacts.delete_card()

Delete a contact card

Scopescontacts:write
Signature
def delete_card(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> DeletedContactCardResource

Removes the card from the address book and from every phone and computer that syncs it. On a saved contact it is the same as delete: the contact goes with its notes, photo and audiences, and the address is hidden from the people list and the suggestions.

Parameters

idstrRequired

The card id, ccd_ followed by 24 characters, from list_cards or get_card.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

DeletedContactCardResource with object set to 'contact_card', id and deleted set to True.

Example

from openemail import openemail removed = openemail.contacts.delete_card('ccd_0123456789abcdef01234567') print(removed['id'], removed['deleted'])

Notes

  • There is no undo, and the SDK does not retry it after a network failure.

  • An id that names no card is a 404 contact_card_not_found.

Also available in

API
DELETE /contacts/cards/{id}
TypeScript
contacts.deleteCard()
Ruby
contacts.delete_card
PHP
contacts->deleteCard
Go
Contacts.DeleteCard
Java
contacts().deleteCard
C#
Contacts.DeleteCardAsync
CLI
openemail contacts delete-card

contacts.find_duplicate_cards()

Find duplicate contact cards

Scopescontacts:read
Signature
def find_duplicate_cards(    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactCardDuplicateListResource

Finds the cards in the address book that look like the same person, as Find duplicates on the Contacts page of the app does: cards that share an email address or a phone number, or that have the same full name. Each group in data lists its cards with the most complete one first, which is the card to keep when you pass the rest to merge_cards.

Groups found by a shared address come first, then by phone number, then by name. Phone numbers match on their last nine digits, so +44 20 7946 0000 and 020 7946 0000 are the same number. Group cards are never compared.

Parameters

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactCardDuplicateListResource, a dict with data, each group a dict with object set to 'contact_card_duplicates', reason, one of CONTACT_CARD_DUPLICATE_REASONS, and cards, and hasMore, True when more than 100 groups were found.

Example

from openemail import openemail found = openemail.contacts.find_duplicate_cards() for group in found['data']:    keep, *others = group['cards']    print(group['reason'], keep['name'], [other['id'] for other in others])

Notes

  • It reads every card, so call it when somebody asks rather than on every page load.

  • With hasMore, merge the groups you have and call it again for the rest.

Also available in

API
GET /contacts/cards/duplicates
TypeScript
contacts.findDuplicateCards()
Ruby
contacts.find_duplicate_cards
PHP
contacts->findDuplicateCards
Go
Contacts.FindDuplicateCards
Java
contacts().findDuplicateCards
C#
Contacts.FindDuplicateCardsAsync
CLI
openemail contacts find-duplicate-cards

contacts.merge_cards()

Merge contact cards

Scopescontacts:write
Signature
def merge_cards(    id: str,    body: ContactCardMerge,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactCardResource

Folds other cards into the card id names and deletes them, as Merge does in Find duplicates on the Contacts page of the app. Fields the kept card leaves empty are filled from the others, notes are joined, and every email address, phone number, postal address and website is kept. Groups that held the other cards hold the kept one instead.

A saved contact among the others stays known by its address, in the people list and in its audiences, but it leaves the address book. When the kept card has no address of its own, it becomes the saved contact of the first address the others had. Phones and computers that sync the address book over CardDAV get the change on their next sync.

Parameters

idstrRequired

The card to keep, from list_cards, get_card or find_duplicate_cards.

body['cardIds']list[str]Required

The cards to fold into it, 1 to 10 of them. They are deleted once their details are kept.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactCardResource for the card that kept everything.

Example

from openemail import openemail card = openemail.contacts.merge_cards(    'ccd_0123456789abcdef01234567', {'cardIds': ['ccd_89abcdef0123456789abcdef']}) print(card['name'], len(card['emails']), 'other addresses')

Notes

  • There is no undo, and the SDK does not retry it after a network failure.

  • Naming the kept card among cardIds, or a group anywhere, is a 422 invalid_contact_card, and an id that names no card a 404 contact_card_not_found.

Also available in

API
POST /contacts/cards/{id}/merge
TypeScript
contacts.mergeCards()
Ruby
contacts.merge_cards
PHP
contacts->mergeCards
Go
Contacts.MergeCards
Java
contacts().mergeCards
C#
Contacts.MergeCardsAsync
CLI
openemail contacts merge-cards

contacts.list_birthdays()

List birthdays

Scopescontacts:read
Signature
def list_birthdays(    *,    from_: datetime | str,    to: datetime | str,    timezone: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ContactBirthdayResource]

Returns the birthdays of the people in the address book that fall between from_ and to, the birthdays calendar the Calendar page of the app can show. There is one entry for each date, so a window of a whole year can hold somebody twice, and age is how old they turn that day when the card holds the year of birth. Somebody born on 29 February has their birthday on 28 February in other years.

from_ and to take a datetime or an ISO 8601 string and may be at most 366 days apart. timezone, UTC unless you say, decides which day the window starts and ends on.

Parameters

from_datetime | strRequired

The start of the window, a datetime or an ISO 8601 string.

todatetime | strRequired

The end of the window, after from_ and at most 366 days later.

timezonestr

IANA zone such as Europe/London, at most 64 characters. Defaults to UTC.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ContactBirthdayResource] sorted by date and then by name, each with cardId, contactId, email, name, birthday, date, age and photoUrl.

Example

from datetime import datetime, timedelta, timezone from openemail import openemail start = datetime.now(timezone.utc)birthdays = openemail.contacts.list_birthdays(    from_=start, to=start + timedelta(days=30), timezone='Europe/London') for birthday in birthdays:    print(birthday['date'], birthday['name'], birthday['age'] or 'age not known')

Notes

  • A value that does not parse, a reversed or overlong window, or an unknown zone is a 400 invalid_parameter naming the field.

Also available in

API
GET /contacts/birthdays
TypeScript
contacts.listBirthdays()
Ruby
contacts.list_birthdays
PHP
contacts->listBirthdays
Go
Contacts.ListBirthdays
Java
contacts().listBirthdays
C#
Contacts.ListBirthdaysAsync
CLI
openemail contacts list-birthdays

contacts.import_vcf()

Import a contacts file

Scopescontacts:write
Signature
def import_vcf(    data: RawBody,    *,    filename: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> ContactImportResource

Sends a vCard (.vcf) file as the request body and queues it. Every card in it becomes a contact card, with its phone numbers, addresses and the rest. Poll get_import until status is completed.

The file may be 20 MB. Imported contacts join no audience, so a broadcast never reaches them because of an import. A card whose address or UID is already in the address book is counted under merged and left as it is, and a card with no address is matched on its phone number. A workspace keeps 20,000 cards, and the cards past that are counted under skipped.

data is bytes, a bytearray or a memoryview, and it goes out as text/vcard.

Parameters

dataRawBodyRequired

The .vcf file as bytes, a bytearray or a memoryview.

filenamestr

The name to show for the file in the list of imports, at most 255 characters.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactImportResource with status set to queued.

Example

from pathlib import Path from openemail import openemail queued = openemail.contacts.import_vcf(    Path('contacts.vcf').read_bytes(), filename='contacts.vcf') print(queued['id'], queued['status'])

Notes

  • A body that holds no vCard is a 400 not_a_contacts_file, and an empty one a 400 item_import_empty.

  • A file over 20 MB is a 413 item_import_too_large.

  • To send text you already hold, encode it first with text.encode().

  • An upload that outlasts the client's timeout raises OpenEmailNetworkError with is_timeout, so pass a longer timeout= for a large file on a slow connection.

  • Not retried automatically by the SDK.

Also available in

API
POST /contacts/imports
TypeScript
contacts.importVcf()
Ruby
contacts.import_vcf
PHP
contacts->importVcf
Go
Contacts.ImportVcf
Java
contacts().importVcf
C#
Contacts.ImportVcfAsync
CLI
openemail contacts import-vcf

contacts.list_imports()

List contacts imports

Scopescontacts:readPages through results
Signature
def list_imports(    *,    limit: int | None = None,    cursor: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Page[ContactImportResource]

Returns one page of the contacts files and address books brought in, newest first, each with what it added and what it left out. Beside the files sent with import_vcf it lists the address books that came with a mailbox import, and those carry the id of that import in parentImportId.

Paging is keyset. limit= takes 1 to 100 and defaults to 25, and nextCursor goes back as cursor= while hasMore is True. list_all_imports and iterate_imports do that walk for you.

Parameters

limitint

Imports per page, a whole number from 1 to 100. The server defaults to 25.

cursorstr

The nextCursor from the previous page. Never build one yourself.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Page[ContactImportResource], a dict with items, hasMore and nextCursor.

Example

from openemail import openemail page = openemail.contacts.list_imports(limit=50) for item in page['items']:    counts = item['counts']    print(item['fileName'], item['status'], counts['imported'], counts['merged'])

Notes

  • source is file for an upload. takeout, carddav and microsoft came with a mailbox import.

  • A cursor that names no import is a 400 invalid_cursor.

  • A GET is retried automatically on network failure and on 408, 500, 502, 503 and 504 responses, up to the client's max_retries, and on a 429 only when it carries a Retry-After of a minute or less.

Also available in

API
GET /contacts/imports
TypeScript
contacts.listImports()
Ruby
contacts.list_imports
PHP
contacts->listImports
Go
Contacts.ListImports
Java
contacts().listImports
C#
Contacts.ListImportsAsync
CLI
openemail contacts list-imports

contacts.list_all_imports()

Collect every contacts import into one list

Scopescontacts:readPages through results
Signature
def list_all_imports(    *,    limit: int | None = None,    cursor: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ContactImportResource]

Follows nextCursor from page to page and returns every contacts import in one list, newest first.

Everything is held in memory before the call returns. Prefer iterate_imports when you can stop early. limit= sets the page size of each request, not the total.

Parameters

limitint

Page size per request, from 1 to 100, defaulting to 25 on the server.

cursorstr

A cursor from an earlier page to start after.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ContactImportResource] holding every import across all pages.

Example

from openemail import openemail imports = openemail.contacts.list_all_imports()added = sum(item['counts']['imported'] for item in imports) print(f'{added} cards came from {len(imports)} imports')

Notes

  • A failure on any page raises out of the whole call, and the imports already fetched are discarded.

Also available in

API
GET /contacts/imports
TypeScript
contacts.listAllImports()
Ruby
contacts.list_all_imports
PHP
contacts->listAllImports
Go
Contacts.ListAllImports
Java
contacts().listAllImports
C#
Contacts.ListAllImportsAsync

contacts.iterate_imports()

Stream contacts imports one at a time

Scopescontacts:readPages through results
Signature
def iterate_imports(    *,    limit: int | None = None,    cursor: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[ContactImportResource]

Returns a generator that yields contacts imports one at a time, newest first, and requests the next page only once the current one is used up. Nothing is fetched until the loop starts, and leaving the loop with break stops the requests.

Parameters

limitint

Page size per request, from 1 to 100, defaulting to 25 on the server.

cursorstr

A cursor from an earlier page to start after.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[ContactImportResource], a generator yielding one import per step.

Example

from openemail import openemail for item in openemail.contacts.iterate_imports():    if item['status'] != 'running':        continue     print('still running', item['id'], item['counts']['total'])    break

Notes

  • The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in

API
GET /contacts/imports
TypeScript
contacts.iterateImports()
Ruby
contacts.iterate_imports
PHP
contacts->iterateImports
Go
Contacts.IterateImports
Java
contacts().iterateImports
C#
Contacts.IterateImportsAsync

contacts.get_import()

Get a contacts import

Scopescontacts:read
Signature
def get_import(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactImportResource

Returns one contacts import with its counts and its report: the files it read, how many cards it left out and why, and up to 20 of the cards it left out. Poll it after import_vcf until status is completed or failed.

counts['imported'] is what was added, counts['merged'] the cards left as they were because they were already in the address book, and counts['skipped'] what was left out, which report['skipped'] breaks down by reason: limit for a card past the 20,000 a workspace keeps, invalid for one that could not be read and empty for one that held nothing.

Parameters

idstrRequired

Contacts import id, iimp_ followed by 24 hex characters.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactImportResource with status, counts, report, undoable and when it started, finished and was undone.

Example

from openemail import openemail item = openemail.contacts.get_import('iimp_7b1e4d8f60a5c7b92d3f9c2a')counts = item['counts'] print(item['status'], counts['imported'], counts['merged'], counts['skipped']) for sample in item['report']['samples']:    print(sample['name'], sample['reason'])

Notes

  • An id that names no contacts import is a 404 item_import_not_found, which raises OpenEmailApiError with is_not_found.

  • A GET is retried automatically on network failure and on 408, 500, 502, 503 and 504 responses, up to the client's max_retries, and on a 429 only when it carries a Retry-After of a minute or less.

Also available in

API
GET /contacts/imports/{id}
TypeScript
contacts.getImport()
Ruby
contacts.get_import
PHP
contacts->getImport
Go
Contacts.GetImport
Java
contacts().getImport
C#
Contacts.GetImportAsync
CLI
openemail contacts get-import

contacts.undo_import()

Remove what a contacts import added

Scopescontacts:write
Signature
def undo_import(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ContactImportResource

Deletes every contact and card the import added, changed since or not, and marks the import undone. Contacts that were already in the address book are not touched.

Parameters

idstrRequired

Contacts import id, iimp_ followed by 24 hex characters.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ContactImportResource with status set to undone and undoneAt set.

Example

from openemail import openemail undone = openemail.contacts.undo_import('iimp_7b1e4d8f60a5c7b92d3f9c2a') print(undone['status'], undone['undoneAt'], undone['counts']['imported'])

Notes

  • An import that is still running, or was already removed, is a 409 item_import_not_undoable. undoable on the import says whether the call would remove anything.

  • An id that names no contacts import is a 404 item_import_not_found.

  • Not retried automatically by the SDK.

Also available in

API
POST /contacts/imports/{id}/undo
TypeScript
contacts.undoImport()
Ruby
contacts.undo_import
PHP
contacts->undoImport
Go
Contacts.UndoImport
Java
contacts().undoImport
C#
Contacts.UndoImportAsync
CLI
openemail contacts undo-import