Skip to the documentation
TypeScript

Changelog

Every release of the package, newest first.

0.0.9

The ninth release adds 73 methods, 558 in all: automations and contact events, muting, inbox tabs, reminders, out of office replies, labels that AI applies, two sending lanes, an address checker, dedicated sending IPs, and imports of a live mailbox, a calendar file and a contacts file. A key can read its own request log with no scope at all, and the request log filters by status code and by route.

  • automations is new: emails and other steps that run by themselves for each contact, such as a welcome series when somebody joins an audience. An automation has a trigger and steps, which send an email from a template, wait, branch, add to or remove from an audience, write a custom field, call a webhook or exit. You edit a draft definition and publish it, which saves a numbered version and sets the automation live, and each contact stays on the version they entered on.
  • automations.list, listAll, iterate, get, create, update and delete manage them, listStarters returns ready-made definitions, and pause, resume, archive and duplicate change their state. sendTest sends the email of one step to one address, listVersions and restoreVersion read the published versions and copy one back into the draft, and stats returns totals, per-step numbers and a daily series.
  • automations.listEnrollments, listAllEnrollments, iterateEnrollments and getEnrollment read who is in an automation and what each step did for them, enroll puts a contact in, and exitEnrollment takes one out. Reading needs automations:read and every change needs automations:write, and publish, resume and sendTest also need emails:send. A draft that cannot run is refused with 422 invalid_automation, and problems on the error lists every problem that blocks it.
  • events is new: it records what your contacts did, from your own code. send stores one event against a contact, starts every live automation whose trigger names it and ends the waits that were holding out for it, sendBatch records up to 100 in one call and reports per event, and listNames returns the event names the workspace has seen. Both sends carry an Idempotency-Key, generated per call or passed as options.idempotencyKey, so a retry never records an event twice, and contacts.listEvents, listAllEvents and iterateEvents read the events of one contact. Sending needs contacts:write, reading the events of a contact needs contacts:read and listNames needs automations:read, and a key or an app may send 600 events a minute.
  • WEBHOOK_EVENTS gained automation.entered, automation.exited, automation.paused and automation.webhook, so WebhookEvent is wider. Like form.*, they belong to the whole workspace, have to be named in eventTypes, and never reach an endpoint limited to some addresses or domains.
  • threads.mute and threads.unmute mute a conversation and take the mute off, as Mute does in the app. A muted thread in the inbox moves to the archive, and new mail in it goes straight to the archive, unread, with no push, sound or desktop notice. Muting is for the whole workspace, a muted thread carries the built-in MUTE label and is:muted finds it in query, and both methods need threads:write.
  • The inbox has tabs: Primary, Promotions, Updates, Social and Forums. threads.list, listAll and iterate take category to read one tab, category:promotions filters the same way in query, and threads.counts returns categories, the conversations and unread conversations of the inbox in each tab.
  • threads.listSenderCategories, setSenderCategory and clearSenderCategory manage the senders whose mail always goes to one tab, whatever the sorting rules would say. Saving a choice also moves the inbox threads whose newest message is from that sender, up to the newest 1,000. Reading needs threads:read and every change needs threads:write, and a key limited to particular addresses or domains can read the choices but not change them.
  • followUps is new: the reminders on mail you sent. set asks for a thread to come back to the top of the inbox, unread, with the built-in FOLLOW_UP label and a push, if nobody replies by remindAt, list reads the reminders still waiting and the ones that ended, and cancel cancels one. A real reply closes the reminder by itself, and a thread holds one waiting reminder per person, so setting another moves the time. emails.send and each message of emails.sendBatch take followUpAt, a Date, an ISO 8601 instant or a duration such as P3D, to set a reminder as the message goes out. Reading needs threads:read and every change needs threads:write.
  • outOfOffice is new: the automatic reply an address sends while its owner is away. set saves the reply of one address, or of a catch-all with *@example.com, with its message, subject, start, end and contactsOnly, replacing whatever was saved, list reads every reply saved in the workspace, or the reply of one address with address, and clear removes one. While a reply is active, each person who writes gets the message once for the whole away period, and spam, bulk mail, automatic mail and bounces get no reply. Reading needs settings:read and every change needs settings:write.
  • Labels can be applied by AI: labels.create and labels.update take aiInstructions, what the label is for in plain words, and aiEnabled, which has AI read each arriving message against them and add the label when it fits. It only labels and never archives or moves mail, turning it on needs instructions and a paid plan, and a workspace may have 10 such labels. labels.test runs the AI over the newest 20 inbox threads and returns the ones the label would catch, without labelling anything. It needs labels:write and threads:read, and spends one AI action.
  • settings.get and settings.update carry three more account settings: inboxTabs shows the inbox as tabs, primaryOnlyAlerts keeps pushes, sounds and desktop notifications to mail sorted into Primary while tabs are on, and phoneFollowUps sends a push when a reminder comes due.
  • Mail travels in two lanes, so a newsletter cannot hold up or harm app mail. emails.send, each message of emails.sendBatch and templates.send take stream: transactional, the default, for mail one person is waiting for, or broadcast for newsletters. Every EmailResource carries the stream it went out on, and every copy of a broadcast is on the broadcast lane.
  • sending is new: it reads the lanes. listStreams resolves both with their state and their numbers for the last 7 days, which are recipients, bounces, complaints and the two rates. The broadcast lane pauses itself above 4% bounces or 0.2% complaints, a send to it is then refused with 409 stream_paused, and app mail keeps going. resumeStream resumes it, for the workspace owner, and asks an app for a verification code, and reading needs emails:read and resuming needs domains:write.
  • tools.checkAddress says whether an address is worth sending to before the first message: valid, risky, invalid or unknown, with the reasons. It checks the syntax, whether the domain takes mail, throwaway domains and shared addresses such as info@, and suggests a correction for a likely typo. With emails:read it adds what the workspace knows: whether it suppressed the address and when it last delivered to it or saw it bounce. deep also asks whether the mailbox exists, billed per address through pay as you go, and needs emails:send, and tools.checkAddresses checks up to 100 in one call.
  • A workspace on Business or Enterprise that sends at least 100,000 emails a month can buy a dedicated sending IP. billing.listAddOns says whether it is for sale, what it costs, whether the workspace qualifies and whether it has one, billing.startAddOnCheckout resolves the link where the person pays, and billing.cancelAddOn ends it at the end of the month already paid for. sending.listDedicatedIps and sending.getDedicatedIp read each IP with its status and warmupPercent, since a new IP is warmed up over several weeks. The billing calls need billing:read and billing:write, belong to the workspace owner and ask an app for a verification code, and reading the IPs needs domains:read.
  • domains.getMailAppSettings returns smtpSubmission, the SMTP server on port 587 for apps, devices and libraries that only offer STARTTLS. Its security is starttls, a new member of MAIL_APP_SECURITIES, and it takes the same username and app password as smtp.
  • imports can read a live mailbox, as the Migrations page of the app does. discoverMailbox finds the mail server for an address and says what the mailbox signs in with, checkMailbox signs in once and lists the folders and how many messages an import would bring, and connectMailbox starts copying the mailbox into one address, with contacts and calendars to bring those across too. The old mailbox is only ever read, a message the address already holds is skipped, and the password is kept sealed until the import finishes and for 30 days at most.
  • An import waits by itself when the old provider slows it down: status is parked and remote.resumeAt says when. When the password stops working it is needs-password, and imports.resume sends a new one. These calls need threads:write, with contacts:write and calendar:write for those parts, ImportResource gains source, remote and children, and ImportFormat gains imap and microsoft.
  • calendar.importIcs and contacts.importVcf take an iCalendar or vCard file as bytes and queue it. Events become events of your own and nobody on them is sent anything, cards become contact cards that join no audience, and what is already here is left as it is and counted, so sending a file twice adds nothing. listImports, listAllImports and iterateImports read the imports of each kind, getImport reads one with its counts and a report of what it left out and why, and undoImport deletes everything an import added. Reading needs calendar:read or contacts:read, and the rest needs calendar:write or contacts:write.
  • me.listRequests, me.listActivity and me.stats let a key read its own request log, its own history and its own figures with no scope at all, so a key that can only send can still watch itself. They read the calling key and nothing else: keys.listRequests, keys.listWorkspaceRequests and keys.stats are still the ones that reach other keys, behind keys:read. me.stats fills sends only when the key holds emails:read and leaves it null without that scope, and an OAuth access token is refused with 403 api_key_only, because calls made with one are not logged.
  • The request log takes two more filters on keys.listRequests, keys.listWorkspaceRequests, me.listRequests and their listAll… and iterate… twins. statuses keeps the calls answered with one of up to 20 exact HTTP status codes, and path keeps the calls to one route, whatever the method. path matches the route, not the exact path: an id counts as :id and an email address as :address, and a path that ends in * reads every route that starts with it.
  • A sign-up form field can fill more of the contact: mapsTo also takes phone, company, jobTitle, website and birthday, saved on the contact card when the sign-up is accepted and only where the card has nothing yet. A single contact now comes with signUps, its last 20 sign-ups with their answers, for a caller that holds forms:read.

0.0.8

The eighth release adds 203 methods, 485 in all: contact cards, calendar links and events, mail apps, the knowledge base, AI writing, billing, workspaces, exports, forwarding and more. A role can reach every address, a domain can carry the logo inboxes show next to its mail, and new namespaces manage the web app address, the workspace brand and sign-up forms.

  • addresses:all joins the permission vocabulary as the sixth console-only permission. A role holding it reaches every address on every domain of the workspace, including ones added later, without an address or domain grant, and sends as any of them when it also holds emails:send.
  • ConsolePermission includes it, and roles.listPermissions returns it with scope: false under the new addresses group. No key or access token can hold it, so it goes on a role in the app and is only read through the API.
  • An access token acting for a member needs workspace:manage in that member’s role for domains.delete and domains.deleteAddress, as removing one in the app does, and is refused with 403 insufficient_authority without it.
  • The webhooks methods that create, change, test or remove an endpoint, rotate its secret, or read or replay one delivery accept an access token of a member whose role holds addresses:all, where only the owner’s was accepted before. That token is held to the domains the workspace has now, so it is refused with 422 capability_unsupported on an endpoint with no allowlists, unless webhooks.update gives it some, and on a webhooks.create without them. Those still need the owner’s app or a workspace key. The MCP server’s delivery tools do not narrow it that way: there an app such a member connected with every address reads and replays the deliveries of every endpoint.
  • members.resendInvitation is refused with 403 insufficient_authority when the invitation’s role holds a permission the key or access token does not, the rule members.add already applies, and an access token acting for a member is also refused when the invitation carries addresses or domains that member does not reach.
  • The members.list docs say how to read a member whose permissions hold addresses:all: they reach every address, and their addresses and domains list only the grants made to them directly, which may be none.
  • The roles.create docs no longer say a console-only permission is accepted on a role. A key or access token adding one is refused with 403 insufficient_authority, as it always was, and the roles.update, members.add and members.update docs now say so too.
  • The roles.listPermissions docs no longer say api-keys:read and api-keys:write decide who may mint keys: API keys stay with the workspace owner whatever a role holds.
  • The addresses.list docs say an app a member connected always reports unrestricted: false, because it is held to what that member reaches.
  • domains.getLogo reads the logo inboxes show next to mail from a domain, with its mark certificate, what public DNS publishes for it and, for a subdomain, the DMARC of its parent domain. domains.setLogo, removeLogo, setLogoCertificate and removeLogoCertificate change them, and each says in records what happened to the DNS record.
  • domains.update takes dmarcPolicy, which is none, quarantine or reject, since inboxes show a logo only at quarantine or reject. Every DomainResource carries logo and dmarcPolicy.
  • domains.setAddressPhoto and removeAddressPhoto set and remove the photo of an address, and DomainAddressResource carries photoUrl.
  • New types: DomainLogo, DomainLogoCertificate, DomainLogoStatusResource, DomainLogoChangeResource, DomainLogoSet, DomainLogoCertificateSet, DomainLogoMarkKind, DomainLogoRecordOutcome, DomainParentDmarc, DomainDmarcPolicy and DomainAddressPhotoOptions, with the DOMAIN_DMARC_POLICIES, DOMAIN_LOGO_MARK_KINDS and DOMAIN_LOGO_RECORD_OUTCOMES sets.
  • forms is new: sign-up forms that add the people who fill them in to audiences. list, listAll, iterate, get, create, update, delete, publish, pause, resume, duplicate, analytics, listStarters and getStarter manage the forms, and listSubmissions, listAllSubmissions, iterateSubmissions, getSubmission, deleteSubmission, deleteSubmissions, approveSubmission and resendConfirmation manage what people sent. forms.delete asks an OAuth access token for a verification code.
  • forms.subscribe signs someone up through a published form and sends no credential. A missing or invalid answer is refused with 422 invalid_form_submission, and NextmailApiError now carries fields, the problems that refusal lists, and body, the parsed error response.
  • The new scopes are forms:read and forms:write. approveSubmission also needs contacts:write, and resendConfirmation also needs emails:send, as does a call that makes a form send mail. The webhook events form.submitted and form.confirmed carry FormSubmittedEventData and FormConfirmedEventData.
  • FORM_STATUSES, FORM_SUBMISSION_STATUSES, FORM_FIELD_TYPES, FORM_STARTER_SLUGS, FORM_CONFIRM_SOURCES and the other FORM_* sets hold the values and are exported from the package root.
  • appHost is new: the web app address of the workspace, a subdomain on one of its verified domains or any other domain it controls, where its people open Nextmail under the workspace brand. get reads it with its status and the DNS records to publish, set sets or replaces it, verify checks it straight away, and delete removes it, asking an OAuth access token for a verification code. It needs a paid plan. Reads need domains:read and changes need domains:write.
  • branding is new: the brand of the workspace, the settings on its Customisations page. get reads its mark, its logos, its sign-in photo, its fonts and its sign-in background, update changes the fonts and the background, uploadImage uploads one of the four images, and removeImage removes one. Reads need settings:read and changes need settings:write, and a personal space is refused with 409 branding_unavailable.
  • contacts.listCards, listAllCards, iterateCards, getCard, createCard, updateCard and deleteCard read and write the address book as contact cards, with phone numbers, postal addresses, websites, organisation, job title, birthday and the name in parts. Phones and computers that sync the address book over CardDAV get every change on their next sync.
  • calendar.listFeeds, createFeed, resetFeed and deleteFeed manage secret links that publish the calendar to Google Calendar, Outlook or Apple Calendar, and listSubscriptions, createSubscription, updateSubscription, refreshSubscription and deleteSubscription manage the calendars you subscribe to by link.
  • calendar.createEvent, updateEvent, deleteEvent, cancelEvent and respondToEvent put events on the calendar, call off a meeting and answer an invitation. respondToEvent can propose a new time, declineProposal keeps the original one, and threads.getEvent reads the event a thread carries. Every event carries subscriptionId, rdates and recurrenceId.
  • domains.getMailAppSettings, listAppPasswords, createAppPassword, updateAppPassword, deleteAppPassword and sendMailAppSetup put an address into Apple Mail, Gmail, Outlook, Thunderbird or any other mail app over IMAP, SMTP and POP3, and its calendar and contacts over CalDAV and CardDAV. updateMailAppSettings turns each kind of connection on or off.
  • domains.update takes senderName, the name mail from a domain goes out under when the sending address has no name of its own, and domains.updateAddress takes destination to forward the mail of an address without keeping a copy.
  • domains.listAddressForwards, addAddressForwards, updateAddressForward, deleteAddressForward and resendAddressForwardConsent manage where the mail of an address is forwarded, and getAddressLogin, setAddressLogin and deleteAddressLogin the password that signs in as one address alone.
  • knowledge is new: the notes, files and web pages the AI uses when it writes replies, drafts email and answers in the assistant, for the whole workspace, a domain or one address. It lists, searches, adds, refreshes and removes items, suggests notes from the replies the workspace sent, flags duplicates and conflicts, and keeps sites, sitemaps, feeds and Zendesk help centers in step through connectors. The new scopes are knowledge:read and knowledge:write.
  • emails.compose, emails.rewrite and emails.suggestSubject write with AI as the composer does, and one description to emails.compose can change a whole email: its body, subject, recipients, send time, sender, template and attachments. emails.check scores a message for spam, phishing and AI writing before it is sent.
  • threads.list, listAll and iterate take semantic to match the words of a query by meaning, and address to keep the threads delivered to one address. threads.replySuggestions reads the replies the reading pane suggests under the latest message of a thread.
  • threads.counts, summary, restore and delete count the conversations in each folder, read the AI summary of a thread, take one back out of the Bin and delete one for good, and threads.listNotes, createNote, updateNote, deleteNote and reorderNotes read and write its private notes.
  • workspaces and account act for the person behind the client: they list, choose, create and delete workspaces, and manage notifications, the profile photo, the username, connected apps and invitations, with the new scopes account:read and account:write.
  • billing reads the plan, the usage, the alerts and the invoices of the workspace, manages pay as you go, and returns the link to a checkout or to the billing portal. Only the workspace owner reaches it, with the new scopes billing:read and billing:write.
  • dnsConnections reads and disconnects the DNS provider accounts connected to the workspace, and domains.getDns, setDnsZone and syncDns read how the DNS of a domain is set up, choose the zone it goes through and write or repair its records.
  • exports makes a zip of the whole workspace, subscriptions manages the newsletters and mailing lists the mailbox receives, analytics reads the numbers behind the Analytics page, and chats manages the conversations held with the assistant.
  • templates.listImages, uploadImage, design and redesign reach the image library and the AI designer of the template editor, forms.design and forms.redesign do the same for sign-up forms, and emails.update changes a message that has not gone yet.
  • senders.get and senders.research read and look up what is known about the organisation behind a sender, encryption publishes and looks up OpenPGP keys, webhooks.listEvents and webhooks.stats describe the events and how deliveries went, and keys.stats reads what the keys did.
  • members.grantDomain and members.revokeDomain give a member a whole domain and take it back, files.revokeAllLinks makes a file private at once, broadcasts.analytics compares the broadcasts sent inside a window, and broadcasts.stats takes days or minutes to report what happened inside one.
  • tools.checkDmarc, checkBimi, checkDeliverability, readVerificationCode and estimateCost are the free checkers of the website, and support.send writes to the people who build Nextmail. None of them needs a scope.
  • providerImports moves an account across from SendGrid, Postmark, Mailgun and Mailchimp as well as Resend, and IMPORT_FORMATS, now exported, includes proton.
  • settings.get and settings.update reach every setting the app has: @domain sets a whole domain, null lets a scope inherit again, and workspaceName renames the workspace. drafts.create takes forwardOf to forward a message, and templates.publish takes expectedVersion.
  • A message whose recipients all bounced reads bounced, and one that bounced for some of them reads partial, where both used to read sent. The messages threads.get returns carry sentWith, forwardedFrom and verificationCode.
  • drafts.update keeps the attachments and the forward of a draft, and the security.verifyStepUp docs list all 40 calls that ask an OAuth access token for a verification code.

0.0.7

The seventh release. The disposable inbox methods are typed as the API answers them, and their docs describe the lease model the API runs.

  • tempMail.listAttachments resolves TempAttachmentResource in place of AttachmentResource: attachmentId, filename, mimeType, size in decoded bytes, body, always an empty string, and headers, always empty. No route on a disposable inbox serves the bytes, so the contentType and content the old type declared never arrived.
  • tempMail.extend resolves ExtendedTempInboxResource, the inbox plus the new token the response always carried. That token holds the later expiry while the old one keeps its old expiry, so use it from then on.
  • The tempMail method docs say what the API does: the inbox token is the lease and nothing about it is stored on the server, deleting an inbox moves the mail it shows to the bin while the lease runs on, reading a message does not mark it seen, and nothing is counted or rate limited.
  • New types: TempAttachmentResource and ExtendedTempInboxResource.

0.0.6

The sixth release. A client can act for a person with an OAuth access token and answer the verification code the API asks for before a sensitive change, and it no longer sends a credential where it could leak.

  • The client takes accessToken in place of apiKey: the token, or a function that returns it and is called before each request, so a renewed token is used without a new client. Passing both, or neither, throws. createClient() reads NEXTMAIL_ACCESS_TOKEN, and isAccessToken checks the shape the API accepts.
  • me.get and me.ping describe an access token as well as a key. A token answers object: 'oauth_token' with a clientId, and both answers carry kind, apiKey or oauth, from CREDENTIAL_KINDS. Code that read id or keyId as a plain string narrows on object or kind first.
  • security.stepUpStatus, beginStepUp and verifyStepUp handle the verification code an access token is asked for before a sensitive change, such as deleting a domain or changing a webhook. One verification holds for 60 minutes. API keys are never asked, and these calls answer one with 400 step_up_not_applicable.
  • Each app can open 5 challenges an hour and 20 a day with each person. Ten wrong codes in 24 hours pause verification for that app, and twenty across all of a person's apps pause it for every app, answered with 429 step_up_locked.
  • NextmailApiError.isStepUpRequired is true when a request was refused with step_up_required. Verify a code, then replay the request once.
  • threads.get returns deliveredTo, the workspace address the thread belongs to, or null when none was recorded.
  • A raw.request path must begin with a single / and stay on the origin of baseUrl, or the call throws before anything is sent.
  • A credential is never attached to a request over plain http: to another machine. localhost, any 127.x.x.x address and ::1 still work, and a baseUrl on 0.0.0.0 or [::] throws and names the address to use instead.
  • New types: AccessTokenProvider, ApiKeySelfResource, OauthTokenSelfResource, ApiKeyPingResource, OauthTokenPingResource, CredentialKind, StepUpStatusResource, StepUpChallengeResource, StepUpVerifiedResource, StepUpBegin, StepUpVerify, StepUpMethod and StepUpErrorCode.
step-up.ts
const nextmail = new Nextmail({ accessToken: () => session.accessToken() }) try {  await nextmail.webhooks.update(id, { enabled: false })} catch (error) {  if (!(error instanceof NextmailApiError) || !error.isStepUpRequired) throw error   await nextmail.security.beginStepUp()  await nextmail.security.verifyStepUp({ code: await askForCode() })  await nextmail.webhooks.update(id, { enabled: false })}

0.0.5

The fifth release. Domains can be added, checked, removed and given addresses from code, audience members can be filtered by whether they are still subscribed, and a file says whether it is public.

  • domains.create adds a domain and returns it with every DNS record to publish. domains.verify checks its DNS straight away, and domains.delete removes it and returns leftBehind, the records that have to be removed by hand. The last domain in a workspace cannot be removed through the API and throws 409 last_domain.
  • domains.get, update, create and verify return records, each DNS record with its type, name, value, priority, purpose and status (found, missing, or null before the first check), and dmarc, the DMARC reading, or null until the domain is verified. DOMAIN_RECORD_STATUSES and DOMAIN_DMARC_STAGES hold the values.
  • domains.update takes catchAll beside trackingHost and storageHost.
  • The addresses on a domain have their own methods. domains.listAddresses, listAllAddresses and iterateAddresses page through them, and domains.createAddress, getAddress, updateAddress (rename it, or turn it off and on) and deleteAddress manage one. Reads need domains:read and changes need domains:write.
  • audiences.listContacts, listAllContacts and iterateContacts take statuses, which keeps only the subscribed members, only the unsubscribed ones, or both. AUDIENCE_MEMBER_STATUSES holds the values.
  • Every FileResource carries visibility, public while a download link to the file works and private otherwise, and publicUrl, a working public link or null. Uploads start private. FILE_VISIBILITIES holds the values.
  • New types: DomainCreate, DomainRecordResource, DomainRecordStatus, DomainDmarcResource, DomainDmarcStage, DomainAddressResource, DomainAddressCreate, DomainAddressPatch, DeletedDomainResource, DeletedDomainAddressResource, AudienceMemberStatus and FileVisibility.
domains.ts
const domain = await nextmail.domains.create({ domain: 'acme.com' })for (const record of domain.records) console.log(record.type, record.name, record.value) const checked = await nextmail.domains.verify(domain.id)await nextmail.domains.createAddress(domain.id, { localPart: 'billing', label: 'Invoices' })

0.0.4

The fourth release. Broadcasts can be followed copy by copy. Files have permissions of their own, can be published at a public link and filtered the way the Files page filters them, and uploads and deletions are announced by webhook.

  • broadcasts.listRecipients, listAllRecipients and iterateRecipients list everybody a broadcast went to, sorted by address, with each copy's status, sentAt, deliveredAt, bouncedAt, complainedAt, opens, clicks and unsubscribedAt. filter keeps one group (BROADCAST_RECIPIENT_FILTERS: pending, sent, delivered, opened, not_opened, clicked, bounced, complained, failed, unsubscribed) and q searches the address and name.
  • broadcasts.getRecipient returns one copy with its subject, html and text exactly as that person got it, and broadcasts.stats returns the totals and a series per grain. All five need emails:read. New types: BroadcastRecipientResource, BroadcastRecipientContentResource, BroadcastRecipientListOptions, BroadcastRecipientFilter, BroadcastStatsResource, BroadcastStatsOptions, BroadcastStatsTotals and BroadcastStatsBucket.
  • API_SCOPES.FILES_READ (files:read) lists, searches, reads and downloads every file, uploads included, with its stats and links. API_SCOPES.FILES_WRITE (files:write) uploads, deletes and publishes files, and includes files:read. Reading an email with threads:read still reads the attachments on it.
  • files.createLink publishes a file at a public download link that opens with no sign-in, on the files host of options.domain when that domain has one, such as files.acme.com. files.revokeLink stops a link from working, including in mail that already went out.
  • files.list, files.listAll and files.iterate take direction (inbound, outbound or uploaded), address (the files of one address), and since and until (a Date or an ISO 8601 string) beside q, kind and sort.
  • Two webhook events: file.uploaded when a file is put on the Files page, and file.deleted when one is deleted. Their data is FileEventData. They are not in the default set, so an endpoint names them in eventTypes, and an endpoint limited to some addresses hears only about the files of those addresses.
  • Two new types: FileLinkCreateOptions and FileEventData.
  • A key limited to particular addresses or domains, and an app the workspace owner connected with such a limit, can now use contacts, audiences and broadcasts, which were a 422 capability_unsupported before. A broadcast's from must still be an address the key may send as. broadcasts.list, get, stats, the recipient methods and cancel reach only the broadcasts sent from an address or domain the key holds, and any other id throws broadcast_not_found. contacts.listPeople returns the saved contacts alone for such a key. contacts.listThreads, contacts.activity, contacts.block and contacts.unblock still refuse it with 422 capability_unsupported, and so does every contacts, audiences and broadcasts method for an app connected by a member who reaches only some addresses.
  • The files.* methods need files:read or files:write where they needed threads:read or threads:write. Keys, roles and app grants that held threads:read or threads:write were given the matching files:* scope on the server, so the change only shows on a key created afterwards without them, which is refused with 403 insufficient_scope.
broadcasts.ts
const stats = await nextmail.broadcasts.stats('brd_5a8c1e3f7b2d94a06c8e1f3b')const bounced = await nextmail.broadcasts.listAllRecipients('brd_5a8c1e3f7b2d94a06c8e1f3b', { filter: 'bounced' })
files.ts
const uploads = await nextmail.files.listAll({ direction: 'uploaded', since: '2026-09-01' }) const link = await nextmail.files.createLink('file_6bb640f5b99e47deb758f1f5', { domain: 'acme.com' })console.log(link.url)

0.0.3

The third release. Files can be uploaded, deleted many at a time and counted from code, and a file is deleted only when nothing depends on it.

  • files.upload stores a file on the Files page, the way its Upload button does, and resolves with the new FileResource. The bytes go as a Blob, ArrayBuffer or Uint8Array, with the name in options.filename and the type in options.contentType or the Blob’s own type. Attach the result to a send as { fileId }. A file can be up to 100 MB, and a workspace keeps up to 10 GB of uploads and takes 500 an hour. The SDK does not retry an upload, because a second attempt after a lost response could store the file twice. It needs threads:write. An upload may take up to 10 minutes, or the client timeoutMs when that is longer, and options.timeoutMs sets its own limit.
  • files.deleteMany deletes up to 100 files in one call and reports the rest instead of failing: deleted and missing hold ids, and kept names each file it kept with its usage and a reason.
  • files.stats returns the numbers on the Analytics tab of the Files page: files and bytes in all and split into received, sent and uploaded, the largest types and addresses, the last 30 days and the download links still working.
  • FileResource gained usage and deletable. deletable is true only for an upload that nothing depends on. Otherwise usage says what keeps the file: received or sent for one that came in or went out on a message, linked for an upload that went out as a download link that still works, and scheduled for one attached to a message that has not gone out yet. FILE_USAGES is exported from the package root beside FILE_DIRECTIONS. Ten types are new: FileUsage, FileUploadOptions, FileBatchDeleteResource, FileKept, FileStatsResource, FileCount, FileTypeCount, FileDayCount, FileAddressCount and FileLinkCount.
  • RequestOptions gained timeoutMs, a limit for one call in place of the client’s.
  • FILE_DIRECTIONS gained UPLOADED ('uploaded'), and files added on the Files page come back from files.list and files.get with direction: 'uploaded'. FileDirection is wider, so a switch or a Record that covers every member has to cover uploaded.
  • audiences.growth counts unsubscribes. Each bucket gained unsubscribed, the contacts that left the audience through a broadcast unsubscribe link in that bucket. Each series gained subscribed, how many of its contacts are still subscribed, and unsubscribed for the window. totals gained subscribed, the contacts still subscribed to at least one of the audiences read, and unsubscribed. A bucket now appears when somebody joined or unsubscribed in it.
  • files.delete deletes only an upload that nothing depends on. A file that was received or sent, or an upload that went out as a download link that still works or waits on a message that has not gone out yet, is refused with 409 file_in_use, where it used to be deleted. Deleting is no longer the workspace owner’s alone: any key or app holding threads:write may delete, so the 403 owner_only is gone. The change is on the server, so 0.0.2 sees it too.
files.ts
import { openAsBlob } from 'node:fs' const file = await nextmail.files.upload(await openAsBlob('report.pdf'), {  filename: 'report.pdf',  contentType: 'application/pdf',}) const result = await nextmail.files.deleteMany([file.id, 'file_6bb640f5b99e47deb758f1f5'])for (const kept of result.kept) console.log(kept.filename, kept.reason)

0.0.2

The second release. An old mailbox can be brought across from an export file, a Resend account can be moved over whole, contacts can be written, grouped into audiences and sent one personalised message each, a domain can carry its own tracking and files names, a key can rotate itself, and no list stops short any more. The changes that break existing code are listed under it.

  • nextmail.audiences is new: named lists of contacts, with list, get, create, update, delete, listContacts, addContact and removeContact. Every workspace has one default audience, the one whose builtin is default, which holds every contact and cannot be deleted.
  • nextmail.contacts can write. create, update and delete join list and get, and each one takes the email address as the id. delete also hides the address, and an address that was not in the book resolves with wasSaved false rather than a 404.
  • nextmail.domains.update() sets or clears a domain's own tracking name and files name, and DomainResource.tracking and DomainResource.storage report where each one stands.
  • nextmail.me.rotate() replaces the calling key's secret and resolves with the new key in full. There is no overlap window, so the old secret stops working the moment the call returns.
  • KeyResource.domainAllowlist narrows a key to whole domains, beside the individual addresses in addressAllowlist. A domain covers every address on it, including ones created after the key was.
  • Attachments can be sent by reference. { fileId } stands beside the inline form, and attachmentDelivery picks between carrying files inside the message, replacing them with download links, or letting their size decide.
  • Ten more webhook events, twenty in all: replies, deliveries, delays, suppressions, downloads, and the three domain events.
  • email.downloaded fires when a person fetches a file that went out as a download link. One link serves everyone the message went to, so the event names no recipient.
  • Six new scopes: contacts:write, audiences:read, audiences:write, keys:write, keys:read and keys:manage.
  • Twelve fields that were loose strings now have nine named types, among them RecipientKind, ContactSource, RuleMatchMode and WebhookDeliveryStatus. They accept and return the same strings as before.
  • nextmail.imports is new: bring an old mailbox across from a Google Takeout archive, an .mbox from Apple Mail or Thunderbird, .eml files, or a .zip or .tgz holding any of those. importFiles creates the import, uploads each file in parts and starts it in one call, and create, uploadChunk, uploadState, start, get, list, listFailures, cancel and deleteUpload are the same steps one at a time. Threads, dates and Gmail labels come across, and imported mail runs no rules, forwards, notifications or webhooks.
  • nextmail.providerImports is new: move a Resend account across. inspect reads a full-access key and returns what an import would bring without storing anything, and create runs the parts you pick: suppressions, segments as audiences, subscribed contacts, templates, and webhooks created switched off. Domains and API keys cannot be moved by anyone, so they arrive as a checklist in the report instead. The key is erased when the run ends.
  • nextmail.webhooks can look into a delivery and send it again. getDelivery returns one attempt with the body that was sent, what your server answered and every other attempt of the same event, and replayDelivery sends that event once more with its original id, one event at a time, refusing with retry_in_progress while an automatic retry of it is being sent and with replay_in_progress while another replay of it is. listAllDeliveries and iterateDeliveries walk the whole delivery log, and WebhookDeliveryResource gained eventId and nextAttemptAt.
  • Webhook deliveries are retried: up to 8 attempts over about 27 and a half hours, where each event was sent once when 0.0.1 shipped, and every retry keeps the event’s original id and createdAt. The change is on the server, so 0.0.1 sees it too.
  • No list stops at its newest rows any more. audiences.list, audiences.listContacts, imports.list and providerImports.list return a Page with listAll and iterate beside them. tempMail.listMessages gained cursor, hasMore and nextCursor, so an inbox past 50 messages shows them all. contacts.listAll and contacts.iterate are new.
  • nextmail.labels does everything the app does with labels. listColors returns the palette of fourteen solids and seven gradients, a colour can be a hex value or a gradient token such as gradient:sunset, update takes a partial patch in which color: null clears the colour, and every label carries threadCount, createdAt and updatedAt.
  • nextmail.audiences does everything the audience page does. addContacts and removeContacts change up to 200 memberships in one call, importContacts creates contacts and adds them to a list up to 500 rows at a time, empty takes everybody out of a list at once, growth reads how many contacts joined each list over a window, and contacts.setAudiences says exactly which lists one contact is in.
  • audiences.listContacts, listAllContacts and iterateContacts take q, source and sort, the search, filter and five orders of the audience page, and every row is an AudienceContactResource carrying addedAt, the moment the contact joined that audience.
  • AudienceResource carries contactCount, counted at the moment of the read, and lastContactAt, when the most recent contact joined the audience.
  • A key limited to particular addresses or domains reads exactly what they reach. emails.list filters before it cuts a page, so every page but the last is full, rules.list and rules.get show only the rules that can act on mail delivered to those addresses, and rules.listRuns only their runs, while rules.create, rules.update, rules.delete and rules.reorder change only rules that act on nothing but mail to those addresses. A recurring event is expanded in full however its BYDAY is written and however long ago the series began. These are changes on the server, so every caller sees them.
  • nextmail.contacts does everything the Contacts page does: listPeople with the people seen in mail, thread counts and a blocked filter, save, deleteMany, setPhoto and removePhoto, block and unblock, listThreads and activity. list takes q and source, and ContactResource gained photoUrl.
  • threads.list, listAll and iterate take sort, dateFrom, dateTo and fromContacts, the orders and filters of the thread list, and every order pages to the end: threads that arrived in the same instant no longer fall between two pages.
  • nextmail.suppressions is new: the Blocked addresses screen, with list, listAll, iterate, get, add and remove, and SUPPRESSION_REASONS naming the three reasons.
  • nextmail.files is new: the Files page, with list, listAll, iterate, get, download, listLinks, listAllLinks, iterateLinks and delete.
  • members.listInvitations, listAllInvitations, iterateInvitations, resendInvitation and revokeInvitation follow an invitation up after add. InvitationResource carries expired, lastSentAt and delivered, which is null when no outcome was recorded for the last send.
  • nextmail.keys is new: the API keys page from code. list, get, create, update, rotate, revoke and delete manage keys, create and rotate return a secret once, and listRequests, listActivity, listWorkspaceRequests and listWorkspaceActivity read the request log and the audit log with the console’s filters. Reading needs the new keys:read scope and every change the new keys:manage scope, and a key never makes or reaches a key wider than itself.
  • nextmail.broadcasts is new: send one message to everybody in one or more audiences, a separate copy for each person. send answers at once and the sending runs in the background, get reads status and live counts, list, listAll and iterate page through past broadcasts, preview counts who a send would reach, and cancel stops the copies that have not gone. subject, html and text take merge fields such as {{firstName|there}}, a template receives the same values as the props it declares, and every copy carries a one-click unsubscribe. send carries an Idempotency-Key, so a retry never sends twice.
  • EmailResource gained broadcastId, and emails.list, listAll and iterate take broadcastId to list the copies of one broadcast. AudienceContactResource gained unsubscribedAt, set when the contact unsubscribed from a broadcast to that audience.
  • nextmail.webhooks reads its logs the way the console does. listDeliveries takes status, since and until, listWorkspaceDeliveries reads every endpoint at once, and listActivity and listWorkspaceActivity read who changed what. WebhookDeliveryResource gained endpointId.
  • nextmail.templates does everything the editor does: duplicate, replaceContent, restoreVersion, deleteVersion, getVersion, listStarters, getStarter and listFonts, create from a starter, render for a body that is not stored anywhere, and getAnalytics and listSends for how a template performed. list takes search and sort, and update takes slug.
  • settings.get and settings.update take an address option, an address or *@domain for a catch-all, to read and set the signature and tracking of that address, and SettingsResource gained address.
  • contacts.get, create and update resolve with ContactDetailResource, the contact and the audiences it is in, and ContactResource gained createdAt and updatedAt.
  • Webhooks can be narrowed to addresses and domains of their own with addressAllowlist and domainAllowlist, and an event about any other address is not delivered to that endpoint. A key limited to particular addresses or domains, which 0.0.1 refused on every webhook write, can now manage webhooks narrowed inside what it holds.
  • Members and invitations can hold whole domains as well as single addresses: MemberAdd takes domainIds, and MemberResource and InvitationResource carry domains. MemberResource.isOwner marks the workspace owner.
  • Fourteen more sets are exported from the package root, among them BROADCAST_STATUSES, SUPPRESSION_REASONS, THREAD_SORTS and WEBHOOK_REPLAY_ERROR_CODES, and every imports and providerImports type can be imported by name.
  • tempMail.listAllMessages and iterateMessages are new, RequestOptions gained raw, contentType and binary, and the update notice links to this page rather than to npm.
  • Contacts belong to the workspace rather than to one person, so a contact saved by any member is one every member sees. The change is on the server, so 0.0.1 sees it too.
audiences.ts
const audience = await nextmail.audiences.create({ name: 'Product updates' }) await nextmail.contacts.create({ email: '[email protected]', name: 'Grace Hopper' })await nextmail.audiences.addContact(audience.id, { email: '[email protected]' })

What breaks in 0.0.2

  • contacts.list returns Page<ContactResource> instead of an array. Read the rows from page.items and follow page.nextCursor while page.hasMore is true. The address book is unbounded, and the old array held one call’s rows, 50 unless limit asked for up to 200, with no way to reach the rest.
  • API_SCOPES, ERROR_TYPES, MESSAGE_ENCRYPTION_FORMATS, RULE_ACTIONS, RULE_FIELDS, RULE_OPERATORS and WEBHOOK_EVENTS are keyed objects rather than arrays, so every member has a name to call it by. The values they held are unchanged.
  • contacts.list sorts contacts that have never been mailed last rather than first, so a contact you just created no longer jumps to the top of the book.
  • webhooks.listDeliveries returns Page<WebhookDeliveryResource> instead of an array and takes limit and cursor. It used to stop at the 50 most recent attempts with no way to reach older ones. Read the rows from page.items and follow page.nextCursor while page.hasMore is true, or collect every attempt with listAllDeliveries.
  • emails.listEvents, tracking.list, tracking.listOpens and tracking.listClicks return a Page instead of an array and take limit and cursor. The tracking lists used to stop at 200 rows with no way further back, and one widely read message could return thousands of events at once. Read page.items and follow page.nextCursor, or use listAllEvents, listAll, listAllOpens and listAllClicks.
  • labels.list returns Page<LabelResource> sorted by name instead of an array, and create and update resolve with the whole LabelResource, so SavedLabelResource is gone. On the server, threads.update refuses an addLabelIds id that names no label with a 422 label_not_found instead of creating one, a label colour must be a hex value or a gradient token, and a name another label already has is a 409.
  • webhooks.list, domains.list, members.list and roles.list return a Page instead of an array and take limit and cursor, with listAll and iterate beside each. addresses.list returns AddressBookPage, the book it returned before plus hasMore and nextCursor, so the addresses come a page at a time, and addresses.listAll collects the whole book. Read page.items and follow page.nextCursor while page.hasMore is true, or use listAll.
  • templates.listVersions and calendar.listEvents return a Page instead of an array and take limit and cursor, with listAllVersions and iterateVersions, and listAllEvents and iterateEvents, beside them. Send the same from, to and timezone with every cursor of calendar.listEvents.
  • The cursors of members.list, roles.list, domains.list, webhooks.list, addresses.list, labels.list, rules.list and templates.list are opaque rather than the id of the last row, so a row deleted between pages no longer makes the next page a 400 invalid_cursor. Pass nextCursor back as it came, since a cursor built from an id is refused.
  • members.add sends an invitation and resolves with InvitationResource (202) instead of MemberResource (201). The person becomes a member when they accept. An address with no account is invited rather than a 404, somebody already in the workspace is a 422 member_is_owner where 0.0.1 changed their role, and inviting the same address again within ten minutes is a 409 invitation_too_soon. The change is on the server, so 0.0.1 sees it too.
  • A key limited to particular addresses or domains is refused with 422 capability_unsupported on members.add, update, remove, grantAddress and revokeAddress, and on roles.create, update and delete. The change is on the server, so 0.0.1 sees it too.
  • Every workspace cap answers 422 workspace_limit_reached, where webhooks.create, templates.create, rules.create and roles.create answered webhook_limit_reached, template_limit_reached, rule_limit_reached and role_limit_reached. The change is on the server, so 0.0.1 sees it too.
  • The signature and tracking belong to each address. settings.update without an address refuses signature, openEmailSignature, trackOpens and trackClicks with 422 address_required. The change is on the server, so 0.0.1 sees it too.
  • AttachmentInput is the union InlineAttachmentInput | StoredAttachmentInput. Passing { filename, content } works as before, but code that reads filename or content from a value typed AttachmentInput has to narrow it first.
  • TemplateContent.document is a TemplateDocument, a body of typed blocks, rather than Record<string, unknown>. TemplateDocument, TemplateBlock and TemplateBlockKind are exported to build one in code.
  • TrackingStatsResource.hitsKnownFrom is gone, and EmailSource no longer includes queue.
  • API_SCOPES gained six scopes and WEBHOOK_EVENTS ten events, so ApiScope and WebhookEvent are wider. A switch or a Record that covers every member has to cover the new ones.
migrate.ts
import { API_SCOPES, WEBHOOK_EVENTS } from '@nextmail/sdk' const scopes = Object.values(API_SCOPES)const subscribable = new Set<string>(Object.values(WEBHOOK_EVENTS)) const page = await nextmail.contacts.list()for (const contact of page.items) send(contact.email)

Code that only used the derived types keeps working, unless it lists every ApiScope or WebhookEvent.

0.0.1

The first release: one typed method for every endpoint, and no runtime dependencies.

  • Sending: one message, batches, scheduled sends, an undo window, and translation into the language the recipient reads.
  • Templates: create, version, publish, preview and send.
  • Tracking: opens, clicks, and a summary for each message.
  • Mailbox: threads, drafts, labels, contacts and rules.
  • Workspace: domains, addresses, members, roles and settings.
  • Webhooks, with a signature verifier that runs anywhere WebCrypto does.
  • Calendar events, including the ICS file.
  • Disposable inboxes, the one part that also runs in a browser.
  • Cursor paging on every list, through listAll and iterate.
  • Two error classes, and retries that cannot duplicate a send.
  • ESM and CommonJS, on Node 20+, Bun, Deno and Cloudflare Workers.