nextmail.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.
nextmail.contacts.list()nextmail.contacts.listAll()nextmail.contacts.iterate()nextmail.contacts.get()nextmail.contacts.create()nextmail.contacts.update()nextmail.contacts.delete()nextmail.contacts.setAudiences()nextmail.contacts.listPeople()nextmail.contacts.listAllPeople()nextmail.contacts.iteratePeople()nextmail.contacts.save()nextmail.contacts.deleteMany()nextmail.contacts.setPhoto()nextmail.contacts.removePhoto()nextmail.contacts.block()nextmail.contacts.unblock()nextmail.contacts.listThreads()nextmail.contacts.listAllThreads()nextmail.contacts.iterateThreads()nextmail.contacts.activity()nextmail.contacts.listEvents()nextmail.contacts.listAllEvents()nextmail.contacts.iterateEvents()nextmail.contacts.listCards()nextmail.contacts.listAllCards()nextmail.contacts.iterateCards()nextmail.contacts.getCard()nextmail.contacts.createCard()nextmail.contacts.updateCard()nextmail.contacts.deleteCard()nextmail.contacts.findDuplicateCards()nextmail.contacts.mergeCards()nextmail.contacts.listBirthdays()nextmail.contacts.importVcf()nextmail.contacts.listImports()nextmail.contacts.listAllImports()nextmail.contacts.iterateImports()nextmail.contacts.getImport()nextmail.contacts.undoImport()
nextmail.contacts.list()
List the workspace contacts, most recently seen first
list(options?: ContactListOptions): Promise<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, and auto for an address recorded when a member sent mail to it from the app composer. A saved contact stays manual when it is mailed later.
options.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. options.q searches the name and the address.
This lists saved contacts only. listPeople lists everyone the Contacts page in the app shows, the addresses seen in mail included, with thread counts.
Paging is keyset. options.limit takes 1 to 200 and defaults to 50, and nextCursor goes back as options.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
options.limitnumberRows per page, a whole number from 1 to 200. The server defaults to 50.
options.cursorstringThe
nextCursorfrom the previous page. Never build one yourself.options.sourceContactSourcemanual,autoorform. Leave it out for the whole book.options.qstringSearches 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.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Page<ContactResource> with items, hasMore and nextCursor. Each contact has email, name, source, notes, attributes, photoUrl and lastSeenAt.
Example
const saved = await nextmail.contacts.list({ source: 'manual', limit: 200 }) console.log(saved.items.map((contact) => contact.name ?? contact.email))Notes
Mail sent through this API adds no contacts. Only sends from the app composer record recipients, and
createis 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
lastSeenAtnull and sorts last, after every contact with a date.Addresses are stored lower cased.
Also available in
nextmail.contacts.listAll()
Collect the whole address book into one array
listAll(options?: ContactListOptions): Promise<Array<ContactResource>>Follows nextCursor from page to page and resolves with every contact in the workspace, 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 promise settles, 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
options.sourceContactSourcemanual,autoorform. Leave it out for the whole book.options.qstringSearches 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.
options.limitnumberPage size per request, from 1 to 200, defaulting to 50 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<ContactResource> holding every contact across all pages.
Example
const saved = await nextmail.contacts.listAll({ source: 'manual', limit: 200 }) console.log(`${saved.length} contacts saved on purpose`)Notes
A failure on any page rejects the whole call, and the contacts already fetched are discarded.
Also available in
nextmail.contacts.iterate()
Stream the address book one contact at a time
iterate(options?: ContactListOptions): AsyncGenerator<ContactResource, void, undefined>Returns an async generator that yields contacts individually, most recently seen first, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.
Parameters
options.sourceContactSourcemanual,autoorform. Leave it out for the whole book.options.qstringSearches 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.
options.limitnumberPage size per request, from 1 to 200, defaulting to 50 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<ContactResource, void, undefined> yielding one contact per step.
Example
for await (const contact of nextmail.contacts.iterate({ source: 'auto' })) { if (contact.lastSeenAt === null) break console.log(contact.email, contact.lastSeenAt)}Notes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
Also available in
nextmail.contacts.get()
Read one contact by email address
get(email: string, options?: RequestScope): Promise<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 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
emailstringRequiredThe contact's address, matched case insensitively.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource with email, name, source, notes, attributes, the custom fields as an object of keys and text values, photoUrl and lastSeenAt, plus audiences, every audience the contact is in as { id, name, builtin }. builtin is default on the audience every contact belongs to and null on one somebody created. A key that also holds forms:read gets signUps, what this address sent through sign-up forms: each entry has id, formId, formName, status (pending or added), answers as { key, label, type, value, display }, sourceUrl, createdAt and confirmedAt.
Example
try { const contact = await nextmail.contacts.get('[email protected]') console.log(contact.name, contact.notes)} catch (error) { if (error instanceof NextmailApiError && error.isNotFound) console.log('not in the address book') else throw error}Notes
nameis null for an address recorded automatically without a display name, andnotesis free text somebody set in the app or throughupdate.The address is the contact's identity here and on every other contacts route. There is no id in the public contract.
signUpsis newest first and holds at most 20 entries. It is left out when the key lacksforms:read, and it is an empty list for an address that never filled in a form.nextmail.forms.listSubmissionspages through every sign-up of one form.
Also available in
nextmail.contacts.create()
Save a contact in the workspace address book
create(body: ContactCreate, options?: RequestScope): Promise<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.addContact. 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.
Parameters
body.emailstringRequiredThe address to save, trimmed and lower cased before it is stored.
body.namestring | nullDisplay name. Leave it out to save the contact without one.
body.notesstring | nullFree text kept with the contact and shown beside it in the app.
body.attributesRecord<string, string | null>Custom fields to set, as an object 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. Null or an empty string removes a field, a field you leave out keeps its value, and a contact holds at most 50.body.audienceIdsArray<string>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.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource with email, name, source as manual, notes, attributes and lastSeenAt, which is null until mail goes to the address from the app composer, plus audiences, every audience the contact is now in, the default one included.
Example
const contact = await nextmail.contacts.create({ email: '[email protected]', name: 'Grace Hopper', notes: 'Met at the compiler workshop', attributes: { plan: 'team' }}) console.log(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_existsmeans the first attempt landed.Contacts belong to the workspace, so the contact this creates is the one every member and every other key sees.
An id in
audienceIdsthat is not an audience of this workspace fails the whole create. Nothing is saved.
Also available in
nextmail.contacts.update()
Change a contact's name, notes or custom fields
update(email: string, patch: ContactPatch, options?: RequestScope): Promise<ContactDetailResource>Changes the fields you send and leaves the rest alone. A key you leave out keeps its stored value, and an explicit null clears it, so { notes: null } empties the notes while {} changes nothing. attributes is merged key by key: a string sets a custom field, null 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
emailstringRequiredThe contact's address, matched case insensitively.
patch.namestring | nullNew display name. Null clears it.
patch.notesstring | nullNew notes. Null clears them.
patch.attributesRecord<string, string | null>Custom fields to set, as an object 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. Null or an empty string removes a field, a field you leave out keeps its value, and a contact holds at most 50.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource as it stands after the change, with audiences alongside it.
Example
const contact = await nextmail.contacts.update('[email protected]', { name: 'Grace Hopper', notes: null, attributes: { plan: 'team', trial: null }}) console.log(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, withparamset toattributes.<key>for a bad key or a value over 500 characters and toattributesfor a 51st field. Nothing is written.Audience membership is not touched here. Use
setAudiencesto set the whole list, oraudiences.addContactandaudiences.removeContactfor one audience.
Also available in
nextmail.contacts.delete()
Delete somebody from the contacts and hide the address
delete(email: string, options?: RequestScope): Promise<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: listPeople 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
emailstringRequiredThe contact's address, matched case insensitively.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
DeletedContactResource, { object: 'contact', email, deleted: true, wasSaved }.
Example
const removed = await nextmail.contacts.delete('[email protected]') console.log(removed.email, removed.deleted)Notes
An address that is not in the book is no longer a 404: it is hidden and answers with
wasSavedfalse. Something that is not an address is a 422invalid_contact.The SDK does not retry a delete, though a second attempt is harmless: it answers with
wasSavedfalse.deleteManydeletes up to 200 addresses in one call.
Also available in
nextmail.contacts.setAudiences()
Set exactly which audiences a contact is in
setAudiences(email: string, body: ContactAudiencesSet, options?: RequestScope): Promise<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.addContact or audiences.removeContact.
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
emailstringRequiredThe contact's address, matched case insensitively.
body.audienceIdsArray<string>RequiredEvery audience the contact should be in, up to 100 ids. A repeated id counts once. More than 100 is a 422
invalid_parameteronaudienceIds, and an empty id is the same error on that entry, such asaudienceIds.0.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource as it stands after the change, with audiences, every audience the contact is now in as { id, name, builtin }, the default one included.
Example
const contact = await nextmail.contacts.setAudiences('[email protected]', { audienceIds: ['aud_9f2c4b7e1a0d63d84c5f2e7b', 'aud_1c4e7a9b2d0f36e85a7c1b4d']}) console.log(contact.audiences.map((audience) => audience.name))Notes
audiences:writeis 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
getfirst when you mean to add one audience to what the contact already has, or useaudiences.addContact.
Also available in
nextmail.contacts.listPeople()
List everyone on the Contacts page, saved or seen in mail
listPeople(options?: PersonListOptions): Promise<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. options.limit takes 1 to 100 and defaults to 25, and nextCursor goes back as options.cursor, with the same sort, q and blocked, while hasMore is true.
Parameters
options.qstringSearches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.
options.emailstringOne address only, matched case insensitively: the way to read one person's thread count and last mail.
options.sortPeopleSortrecent(the default),nameorthreads.options.blockedbooleantruefor only the people the workspace blocklist blocks, whole-domain rules included.options.limitnumberRows per page, 1 to 100. The server defaults to 25.
options.cursorstringThe
nextCursorfrom the previous page. Never build one yourself.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
PeoplePage: 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
const page = await nextmail.contacts.listPeople({ sort: 'threads', limit: 50 }) for (const person of page.items) { console.log(person.name ?? person.email, person.threads, person.saved ? 'saved' : 'seen in mail')}Notes
emailis lower cased and is what every other contacts method takes.displayEmailkeeps 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
seenfalse, even when it holdsthreads:read, because the addresses seen in mail would be read from the mail of every address in the workspace.
Also available in
nextmail.contacts.listAllPeople()
Collect everyone on the Contacts page into one array
listAllPeople(options?: PersonListOptions): Promise<Array<PersonResource>>Follows nextCursor from page to page and resolves with every person listPeople would list, in the same order and with the same filters. It is how you count the people a blocklist blocks: (await nextmail.contacts.listAllPeople({ blocked: true })).length.
Everything is held in memory before the promise settles, and a busy mailbox has seen a great many addresses. Prefer iteratePeople when you can stop early.
Parameters
options.qstringSearches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.
options.emailstringOne address only, matched case insensitively: the way to read one person's thread count and last mail.
options.sortPeopleSortrecent(the default),nameorthreads.options.blockedbooleantruefor only the people the workspace blocklist blocks, whole-domain rules included.options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<PersonResource> holding every person across all pages.
Example
const blocked = await nextmail.contacts.listAllPeople({ blocked: true, limit: 100 }) console.log(`${blocked.length} people are blocked`)Notes
A failure on any page rejects the whole call, and the people already fetched are discarded.
seenis not reported here. Read one page withlistPeopleto learn whether the key reads the people seen in mail.
Also available in
nextmail.contacts.iteratePeople()
Stream everyone on the Contacts page one person at a time
iteratePeople(options?: PersonListOptions): AsyncGenerator<PersonResource, void, undefined>Returns an async generator that yields the people listPeople lists, one at a time, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.
Parameters
options.qstringSearches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.
options.emailstringOne address only, matched case insensitively: the way to read one person's thread count and last mail.
options.sortPeopleSortrecent(the default),nameorthreads.options.blockedbooleantruefor only the people the workspace blocklist blocks, whole-domain rules included.options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<PersonResource, void, undefined> yielding one person per step.
Example
for await (const person of nextmail.contacts.iteratePeople({ sort: 'recent' })) { if (!person.saved && (person.threads ?? 0) > 5) await nextmail.contacts.save(person.email)}Notes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
Also available in
nextmail.contacts.save()
Save an address as a contact, or keep one that was recorded
save(email: string, body?: ContactSave, options?: RequestScope): Promise<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 null 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.
Parameters
emailstringRequiredThe address, trimmed and lower cased on the server.
body.namestringUp to 200 characters. Left out, the stored name is kept.
body.notesstring | nullUp to 5,000 characters.
nullclears the notes; left out, they are kept.body.attributesRecord<string, string | null>Custom fields to set, as an object 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. Null or an empty string removes a field, a field you leave out keeps its value, and a contact holds at most 50.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource, the contact as it stands after the save, with audiences.
Example
const contact = await nextmail.contacts.save('[email protected]', { name: 'Grace Hopper' }) console.log(contact.source)Notes
The server answers 201 for a new contact and 200 for one that was there. The SDK resolves the same way for both, so compare
createdAtwithupdatedAtif 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
nextmail.contacts.deleteMany()
Delete up to 200 contacts in one call
deleteMany(emails: Array<string>, options?: RequestScope): Promise<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
emailsArray<string>Required1 to 200 addresses, matched case insensitively. More than 200, or none, is a 422 on
emails.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactBatchDeleteResource, { object: 'contact_batch_delete', deleted, saved, invalid }. deleted counts the addresses deleted and hidden, saved how many of them were saved contacts.
Example
const result = await nextmail.contacts.deleteMany(['[email protected]', '[email protected]']) console.log(`${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
nextmail.contacts.setPhoto()
Upload the photo shown for a contact
setPhoto(email: string, data: RawBody, options?: ContactPhotoOptions): Promise<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 a Blob, an ArrayBuffer or a Uint8Array. The type is read from options.contentType, or from a Blob's own type when that is left out. Without either the bytes go as application/octet-stream and the server refuses them with 422 invalid_image.
The address has to be a saved contact already: save it first.
Parameters
emailstringRequiredThe contact's address, matched case insensitively.
dataRawBodyRequiredThe image: a
Blob,ArrayBufferorUint8Array.options.contentTypeContactPhotoTypeimage/png,image/jpeg,image/webporimage/gif. Required unlessdatais aBlobwith a type.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource with the new photoUrl.
Example
const file = Bun.file('grace.jpg') const contact = await nextmail.contacts.setPhoto('[email protected]', await file.arrayBuffer(), { contentType: 'image/jpeg' }) console.log(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
nextmail.contacts.removePhoto()
Remove a contact photo
removePhoto(email: string, options?: RequestScope): Promise<ContactDetailResource>Takes the photo off the contact and deletes the stored image, the way Remove does on a contact in the app. The contact answers with photoUrl null. Removing a photo from a contact that has none changes nothing.
Parameters
emailstringRequiredThe contact's address, matched case insensitively.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactDetailResource with photoUrl null.
Example
await nextmail.contacts.removePhoto('[email protected]')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
nextmail.contacts.block()
Block an address
block(email: string, options?: RequestScope): Promise<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
emailstringRequiredThe address to block.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactBlockResource, { object: 'contact_block', email, blocked: true, blockedBy, created }.
Example
const result = await nextmail.contacts.block('[email protected]') console.log(result.created ? 'blocked' : `already blocked by ${result.blockedBy?.rule}`)Notes
It needs
settings:writerather thancontacts: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_unsupportedonaddressAllowlist, because the blocklist belongs to the whole workspace and filters the mail of every address in it.
Also available in
nextmail.contacts.unblock()
Unblock an address
unblock(email: string, options?: RequestScope): Promise<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
emailstringRequiredThe address to unblock.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactBlockResource, { object: 'contact_block', email, blocked: false, removed }, each entry of removed a { rule, list }.
Example
const result = await nextmail.contacts.unblock('[email protected]') for (const hit of result.removed ?? []) console.log(hit.list, hit.rule)Notes
Safe to replay, so the SDK retries it after a network failure.
A key limited to particular addresses or domains gets 422
capability_unsupportedonaddressAllowlist, because the blocklist belongs to the whole workspace and filters the mail of every address in it.
Also available in
nextmail.contacts.listThreads()
List the conversations with one person
listThreads(email: string, options?: ContactThreadListOptions): Promise<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.
options.limit takes 1 to 100 and defaults to 25. Pass nextCursor back as options.cursor, with the same q and sort, while hasMore is true.
Parameters
emailstringRequiredThe address. It does not have to be a saved contact.
options.qstringSearches inside those threads, with the mailbox search syntax, up to 200 characters.
options.sortContactThreadSortnewest(the default),oldest,senderorsubject.options.limitnumberThreads per page, 1 to 100. The server defaults to 25.
options.cursorstringThe
nextCursorfrom the previous page. Never build one yourself.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Page<ContactThreadResource> with items, hasMore and nextCursor.
Example
const page = await nextmail.contacts.listThreads('[email protected]', { q: 'invoice' }) for (const thread of page.items) console.log(thread.receivedAt, thread.subject)Notes
It needs
threads:read, because it reads mail rather than the contact.A key limited to particular addresses or domains gets 422
capability_unsupportedonaddressAllowlist, because a contact's threads and activity are read from the mail of every address in the workspace.
Also available in
nextmail.contacts.listAllThreads()
Collect every conversation with one person into one array
listAllThreads(email: string, options?: ContactThreadListOptions): Promise<Array<ContactThreadResource>>Follows nextCursor from page to page and resolves with every thread listThreads would list for the address, in the same order.
Everything is held in memory before the promise settles. Prefer iterateThreads when you can stop early.
Parameters
emailstringRequiredThe address. It does not have to be a saved contact.
options.qstringSearches inside those threads, with the mailbox search syntax, up to 200 characters.
options.sortContactThreadSortnewest(the default),oldest,senderorsubject.options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<ContactThreadResource> holding every thread across all pages.
Example
const threads = await nextmail.contacts.listAllThreads('[email protected]') console.log(`${threads.length} conversations with Ada`)Notes
A failure on any page rejects the whole call, and the threads already fetched are discarded.
Also available in
nextmail.contacts.iterateThreads()
Stream the conversations with one person one thread at a time
iterateThreads(email: string, options?: ContactThreadListOptions): AsyncGenerator<ContactThreadResource, void, undefined>Returns an async generator that yields the threads listThreads lists for the address, one at a time, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.
Parameters
emailstringRequiredThe address. It does not have to be a saved contact.
options.qstringSearches inside those threads, with the mailbox search syntax, up to 200 characters.
options.sortContactThreadSortnewest(the default),oldest,senderorsubject.options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<ContactThreadResource, void, undefined> yielding one thread per step.
Example
for await (const thread of nextmail.contacts.iterateThreads('[email protected]')) { if (thread.hasUnread) console.log('unread:', thread.subject)}Notes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
Also available in
nextmail.contacts.activity()
Read how mail with one person has gone over a window
activity(email: string, options?: ContactActivityOptions): Promise<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.
options.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 offsetMinutes 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
emailstringRequiredThe address. It does not have to be a saved contact.
options.minutesnumberWindow length in minutes, from 1 to about 20 years. The server defaults to 90 days.
options.grainTrackingGrainBucket width:
minute,hourorday, defaulting today.options.offsetMinutesnumberMinutes east of UTC to cut the buckets in, from -840 to 840, defaulting to 0. Pass
-new Date().getTimezoneOffset()for the local zone.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactActivityResource, { object: 'contact_activity', email, since, until, grain, buckets, totals, replyTime }. totals has received, sent, threads, waiting, lastReceivedAt and lastSentAt; replyTime has yours and theirs, each null when there is no reply to measure.
Example
const activity = await nextmail.contacts.activity('[email protected]', { minutes: 30 * 24 * 60 }) console.log(`${activity.totals.received} in, ${activity.totals.sent} out, ${activity.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_unsupportedonaddressAllowlist, because a contact's threads and activity are read from the mail of every address in the workspace.
Also available in
nextmail.contacts.listEvents()
List the events recorded for one contact
listEvents(email: string, options?: ContactEventListOptions): Promise<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.
options.limit takes 1 to 200 and defaults to 50. Pass nextCursor back as options.cursor, with the same name, while hasMore is true. listAllEvents and iterateEvents do that walk for you.
Parameters
emailstringRequiredThe contact, by email address, compared without case. The
contactIdan event or an enrollment carries works here too.options.namestringOnly events with exactly this name.
options.limitnumberEvents per page, a whole number from 1 to 200. The server defaults to 50.
options.cursorstringThe
nextCursorfrom the previous page, passed back exactly as it came. One that names no event of this contact is a 400invalid_cursor.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Page<ContactEventResource> with items, hasMore and nextCursor. Each item has id, contactId, email, name, properties, occurredAt, mode and createdAt.
Example
const page = await nextmail.contacts.listEvents('[email protected]', { name: 'order.placed' }) for (const event of page.items) console.log(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
nextmail.contacts.listAllEvents()
Collect every event of one contact into one array
listAllEvents(email: string, options?: ContactEventListOptions): Promise<Array<ContactEventResource>>Follows nextCursor from page to page and resolves with every event recorded for the contact in the last 90 days, the most recent first, in the shape listEvents returns. limit sets the page size of each request, not the total.
Parameters
emailstringRequiredThe contact, by email address, compared without case. The
contactIdan event or an enrollment carries works here too.options.namestringOnly events with exactly this name.
options.limitnumberPage size per request, from 1 to 200, defaulting to 50 on the server.
options.cursorstringA
nextCursorfrom an earlier page to start after.options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<ContactEventResource> holding every matching event across all pages.
Example
const events = await nextmail.contacts.listAllEvents('[email protected]') const names = new Set(events.map((event) => event.name)) console.log(events.length, [...names])Notes
A failure on any page rejects the whole call.
Also available in
nextmail.contacts.iterateEvents()
Stream the events of one contact one at a time
iterateEvents(email: string, options?: ContactEventListOptions): AsyncGenerator<ContactEventResource, void, undefined>Returns an async 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 drained. Breaking out of the loop stops the requests.
Parameters
emailstringRequiredThe contact, by email address, compared without case. The
contactIdan event or an enrollment carries works here too.options.namestringOnly events with exactly this name.
options.limitnumberPage size per request, from 1 to 200, defaulting to 50 on the server.
options.cursorstringA
nextCursorfrom an earlier page to start after.options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<ContactEventResource, void, undefined> yielding one event per step.
Example
for await (const event of nextmail.contacts.iterateEvents('[email protected]')) { if (event.name === 'plan.cancelled') { console.log('cancelled on', event.occurredAt) break }}Notes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
Also available in
nextmail.contacts.listCards()
List the address book as contact cards
listCards(options?: ContactCardListOptions): Promise<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, job title and birthday. 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.
options.email finds the card of one saved contact, and options.withoutEmail lists only the cards with no address. An address that was only ever mailed, and never saved, has no card.
Paging is keyset. options.limit takes 1 to 200 and defaults to 50, and nextCursor goes back as options.cursor while hasMore is true.
Parameters
options.limitnumberCards per page, a whole number from 1 to 200. The server defaults to 50.
options.cursorstringThe
nextCursorfrom the previous page. Never build one yourself.options.emailstringOnly the card of the saved contact with this address.
options.withoutEmailbooleanTrue lists only the cards that have no email address.
options.kindContactCardKindgrouplists only the groups,individualonly the people and companies.options.groupIdstringOnly the cards in the group with this id.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Page<ContactCardResource> with items, hasMore and nextCursor.
Example
const page = await nextmail.contacts.listCards({ withoutEmail: true }) for (const card of page.items) console.log(card.name, card.phones.map((phone) => phone.value))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.
Also available in
nextmail.contacts.listAllCards()
Collect every contact card into one array
listAllCards(options?: ContactCardListOptions): Promise<Array<ContactCardResource>>Follows nextCursor from page to page and resolves with every card in the address book, newest first. It takes the same email and withoutEmail filters as listCards.
Everything is held in memory before the promise settles. Prefer iterateCards when you can stop early. limit sets the page size of each request, not the total.
Parameters
options.emailstringOnly the card of the saved contact with this address.
options.withoutEmailbooleanTrue lists only the cards that have no email address.
options.kindContactCardKindgrouplists only the groups,individualonly the people and companies.options.groupIdstringOnly the cards in the group with this id.
options.limitnumberPage size per request, from 1 to 200, defaulting to 50 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<ContactCardResource> holding every card across all pages.
Example
const cards = await nextmail.contacts.listAllCards() console.log(`${cards.length} cards in the address book`)Notes
A failure on any page rejects the whole call, and the cards already fetched are discarded.
Also available in
nextmail.contacts.iterateCards()
Stream the address book one card at a time
iterateCards(options?: ContactCardListOptions): AsyncGenerator<ContactCardResource, void, undefined>Returns an async generator that yields cards one by one, newest first, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.
Parameters
options.emailstringOnly the card of the saved contact with this address.
options.withoutEmailbooleanTrue lists only the cards that have no email address.
options.kindContactCardKindgrouplists only the groups,individualonly the people and companies.options.groupIdstringOnly the cards in the group with this id.
options.limitnumberPage size per request, from 1 to 200, defaulting to 50 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<ContactCardResource, void, undefined> yielding one card per step.
Example
for await (const card of nextmail.contacts.iterateCards()) { if (card.birthday) console.log(card.name, card.birthday)}Notes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
Also available in
nextmail.contacts.getCard()
Get one contact card
getCard(id: string, options?: RequestScope): Promise<ContactCardResource>Resolves one card with everything it holds. To find the card of a saved contact by its address, list the cards with email.
Parameters
idstringRequiredThe card id,
ccd_and 24 characters, fromlistCardsorgetCard.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactCardResource.
Example
const card = await nextmail.contacts.getCard('ccd_0123456789abcdef01234567') console.log(card.name, card.organization, card.phones)Notes
An id that names no card is a 404
contact_card_not_found.
Also available in
nextmail.contacts.createCard()
Add a card to the address book
createCard(body: ContactCardCreate, options?: RequestScope): Promise<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.
Phones and computers that sync the address book over CardDAV get the new card on their next sync.
Parameters
body.emailstring | nullThe address Nextmail 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.body.namestring | nullThe full name the address book shows, up to 200 characters.
body.namePartsContactCardNameInput | nullThe name in parts,
{ prefix, given, middle, family, suffix }, the way phones keep it.body.nicknamestring | nullA nickname.
body.organizationstring | nullThe company or organisation.
body.departmentstring | nullThe department within the organisation.
body.titlestring | nullThe job title.
body.birthdaystring | nullYYYY-MM-DD, or--MM-DDwhen the year is not known.body.notesstring | nullFree-form notes. On a saved contact these are the notes the contact page shows.
body.emailsArray<ContactCardValueInput>Other email addresses besides
email, each{ value, label?, customLabel? }.body.phonesArray<ContactCardValueInput>Phone numbers, each
{ value, label?, customLabel? }.body.addressesArray<ContactCardAddressInput>Postal addresses, each
{ label?, customLabel?, street?, locality?, region?, postalCode?, country? }.body.urlsArray<ContactCardValueInput>Websites and profile links, each
{ value, label?, customLabel? }.body.kindContactCardKindgroupmakes a group, the kind phones keep, which needsnameand takesmembersinstead of an address.individualwhen left out.body.membersArray<string>For a group, the ids of the cards in it, up to 2,000. Groups cannot hold other groups.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactCardResource for the new card.
Example
const card = await nextmail.contacts.createCard({ email: '[email protected]', name: 'Ada Lovelace', organization: 'Analytical Engines', phones: [{ value: '+44 20 7946 0000', label: 'work' }], birthday: '1815-12-10'}) console.log(card.id, card.email)Notes
The SDK does not retry it, 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 422invalid_contact_card.
Also available in
nextmail.contacts.updateCard()
Change a contact card
updateCard(id: string, patch: ContactCardPatch, options?: RequestScope): Promise<ContactCardResource>Changes the fields you send and leaves the rest alone. null clears a field, and a list you send (emails, phones, addresses, 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.
Parameters
idstringRequiredThe card id,
ccd_and 24 characters, fromlistCardsorgetCard.patch.emailstring | nullThe address Nextmail 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.namestring | nullThe full name the address book shows, up to 200 characters.
patch.namePartsContactCardNameInput | nullThe name in parts,
{ prefix, given, middle, family, suffix }, the way phones keep it.patch.nicknamestring | nullA nickname.
patch.organizationstring | nullThe company or organisation.
patch.departmentstring | nullThe department within the organisation.
patch.titlestring | nullThe job title.
patch.birthdaystring | nullYYYY-MM-DD, or--MM-DDwhen the year is not known.patch.notesstring | nullFree-form notes. On a saved contact these are the notes the contact page shows.
patch.emailsArray<ContactCardValueInput>Other email addresses besides
email, each{ value, label?, customLabel? }.patch.phonesArray<ContactCardValueInput>Phone numbers, each
{ value, label?, customLabel? }.patch.addressesArray<ContactCardAddressInput>Postal addresses, each
{ label?, customLabel?, street?, locality?, region?, postalCode?, country? }.patch.urlsArray<ContactCardValueInput>Websites and profile links, each
{ value, label?, customLabel? }.patch.kindContactCardKindWhat 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.membersArray<string>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.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactCardResource as it stands after the change.
Example
const card = await nextmail.contacts.getCard('ccd_0123456789abcdef01234567') const updated = await nextmail.contacts.updateCard(card.id, { phones: [...card.phones, { value: '+1 555 0100', label: 'mobile' }]}) console.log(updated.phones.length)Notes
The SDK retries this call after a network failure, since the same patch sent twice leaves the same card.
Changing the address of a saved contact is a 422
invalid_contact_card.
Also available in
nextmail.contacts.deleteCard()
Delete a contact card
deleteCard(id: string, options?: RequestScope): Promise<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
idstringRequiredThe card id,
ccd_and 24 characters, fromlistCardsorgetCard.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
DeletedContactCardResource, { object: 'contact_card', id, deleted: true }.
Example
const removed = await nextmail.contacts.deleteCard('ccd_0123456789abcdef01234567') console.log(removed.id, removed.deleted)Notes
There is no undo.
Also available in
nextmail.contacts.findDuplicateCards()
Find duplicate contact cards
findDuplicateCards(options?: RequestScope): Promise<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 mergeCards.
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
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactCardDuplicateListResource with data, each { object: 'contact_card_duplicates', reason, cards }, and hasMore, true when more than 100 groups were found.
Example
const found = await nextmail.contacts.findDuplicateCards() for (const group of found.data) { const [keep, ...others] = group.cards console.log(group.reason, keep.name, others.map((card) => card.id))}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
nextmail.contacts.mergeCards()
Merge contact cards
mergeCards(id: string, body: ContactCardMerge, options?: RequestScope): Promise<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
idstringRequiredThe card to keep, from
listCards,getCardorfindDuplicateCards.body.cardIdsArray<string>RequiredThe cards to fold into it, 1 to 10 of them. They are deleted once their details are kept.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactCardResource for the card that kept everything.
Example
const found = await nextmail.contacts.findDuplicateCards()const [group] = found.data if (group) { const [keep, ...others] = group.cards const card = await nextmail.contacts.mergeCards(keep.id, { cardIds: others.map((other) => other.id) }) console.log(card.name, card.emails.length)}Notes
There is no undo, and the SDK does not retry it.
Naming the kept card among
cardIds, or a group anywhere, is a 422invalid_contact_card, and an id that names no card a 404contact_card_not_found.
Also available in
nextmail.contacts.listBirthdays()
List birthdays
listBirthdays(options: ContactBirthdayOptions): Promise<Array<ContactBirthdayResource>>Resolves 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 Date 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
options.fromDate | stringRequiredThe start of the window.
options.toDate | stringRequiredThe end of the window, after
fromand at most 366 days later.options.timezonestringIANA zone such as
Europe/London, at most 64 characters. Defaults toUTC.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Array<ContactBirthdayResource> sorted by date and then by name, each with cardId, contactId, email, name, birthday, date, age and photoUrl.
Example
const from = new Date()const to = new Date(from.getTime() + 30 * 24 * 60 * 60 * 1000) const birthdays = await nextmail.contacts.listBirthdays({ from, to, timezone: 'Europe/London' }) for (const birthday of birthdays) console.log(birthday.date, birthday.name, birthday.age ?? 'age not known')Notes
A missing
fromortois a 422invalid_parameter, while a value that does not parse, a reversed or overlong window, or an unknown zone is a 400invalid_parameternaming the field.
Also available in
nextmail.contacts.importVcf()
Import a contacts file
importVcf(data: RawBody, options?: ItemImportUploadOptions): Promise<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 getImport 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 a Blob, an ArrayBuffer or a Uint8Array, and it goes out as text/vcard.
Parameters
dataRawBodyRequiredThe .vcf file: a
Blob,ArrayBufferorUint8Array.options.filenamestringThe name to show for the file in the list of imports, at most 255 characters.
options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactImportResource with status: queued.
Example
import { readFile } from 'node:fs/promises' const queued = await nextmail.contacts.importVcf(await readFile('contacts.vcf'), { filename: 'contacts.vcf' }) console.log(queued.id, queued.status)Notes
A body that holds no vCard is a 400
not_a_contacts_file, and an empty one a 400item_import_empty.A file over 20 MB is a 413
item_import_too_large.To send text you already hold, encode it first with
new TextEncoder().encode(text).An upload that outlasts the client
timeoutMsfails as a network error, so raise it on the client for a large file on a slow connection.Not retried automatically.
Also available in
nextmail.contacts.listImports()
List contacts imports
listImports(options?: ListOptions): Promise<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 importVcf it lists the address books that came with a mailbox import, and those carry the id of that import in parentImportId.
Paging is keyset. options.limit takes 1 to 100 and defaults to 25, and nextCursor goes back as options.cursor while hasMore is true. listAllImports and iterateImports do that walk for you.
Parameters
options.limitnumberImports per page, a whole number from 1 to 100. The server defaults to 25.
options.cursorstringThe
nextCursorfrom the previous page. Never build one yourself.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
Page<ContactImportResource> with items, hasMore and nextCursor.
Example
const page = await nextmail.contacts.listImports({ limit: 50 }) for (const item of page.items) console.log(item.fileName, item.status, item.counts.imported, item.counts.merged)Notes
sourceisfilefor an upload.takeout,carddavandmicrosoftcame 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
maxRetries, and on a 429 only when it carries aRetry-Afterof a minute or less.
Also available in
nextmail.contacts.listAllImports()
Collect every contacts import into one array
listAllImports(options?: ListOptions): Promise<Array<ContactImportResource>>Follows nextCursor from page to page and resolves with every contacts import, newest first.
Everything is held in memory before the promise settles. Prefer iterateImports when you can stop early. limit sets the page size of each request, not the total.
Parameters
options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
Array<ContactImportResource> holding every import across all pages.
Example
const imports = await nextmail.contacts.listAllImports() const added = imports.reduce((sum, item) => sum + item.counts.imported, 0) console.log(`${added} cards came from ${imports.length} imports`)Notes
A failure on any page rejects the whole call, and the imports already fetched are discarded.
Also available in
nextmail.contacts.iterateImports()
Stream contacts imports one at a time
iterateImports(options?: ListOptions): AsyncGenerator<ContactImportResource, void, undefined>Returns an async generator that yields contacts imports one by one, newest first, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.
Parameters
options.limitnumberPage size per request, from 1 to 100, defaulting to 25 on the server.
options.cursorstringA cursor from an earlier page to start after.
options.signalAbortSignalCancels the request in flight and rejects the whole walk.
options.apiKeystringOverrides the client's API key for every page of this walk.
Returns
AsyncGenerator<ContactImportResource, void, undefined> yielding one import per step.
Example
for await (const item of nextmail.contacts.iterateImports()) { if (item.status !== 'running') continue console.log('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
nextmail.contacts.getImport()
Get a contacts import
getImport(id: string, options?: RequestScope): Promise<ContactImportResource>Resolves 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 importVcf 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
idstringRequiredContacts import id,
iimp_followed by 24 hex characters.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactImportResource with status, counts, report, undoable and when it started, finished and was undone.
Example
const item = await nextmail.contacts.getImport('iimp_7b1e4d8f60a5c7b92d3f9c2a') console.log(item.status, item.counts.imported, item.counts.merged, item.counts.skipped) for (const sample of item.report.samples) console.log(sample.name, sample.reason)Notes
An id that names no contacts import is a 404
item_import_not_found.A GET is retried automatically on network failure and on 408, 500, 502, 503 and 504 responses, up to the client
maxRetries, and on a 429 only when it carries aRetry-Afterof a minute or less.
Also available in
nextmail.contacts.undoImport()
Remove what a contacts import added
undoImport(id: string, options?: RequestScope): Promise<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
idstringRequiredContacts import id,
iimp_followed by 24 hex characters.options.signalAbortSignalCancels the request.
options.apiKeystringOverrides the client's API key for this call only.
Returns
ContactImportResource with status: undone and undoneAt set.
Example
const undone = await nextmail.contacts.undoImport('iimp_7b1e4d8f60a5c7b92d3f9c2a') console.log(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.undoableon 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.