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.
automationsis 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 draftdefinitionandpublishit, 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,updateanddeletemanage them,listStartersreturns ready-made definitions, andpause,resume,archiveandduplicatechange their state.sendTestsends the email of one step to one address,listVersionsandrestoreVersionread the published versions and copy one back into the draft, andstatsreturns totals, per-step numbers and a daily series.automations.listEnrollments,listAllEnrollments,iterateEnrollmentsandgetEnrollmentread who is in an automation and what each step did for them,enrollputs a contact in, andexitEnrollmenttakes one out. Reading needsautomations:readand every change needsautomations:write, andpublish,resumeandsendTestalso needemails:send. A draft that cannot run is refused with 422invalid_automation, andproblemson the error lists every problem that blocks it.eventsis new: it records what your contacts did, from your own code.sendstores one event against a contact, starts every live automation whose trigger names it and ends the waits that were holding out for it,sendBatchrecords up to 100 in one call and reports per event, andlistNamesreturns the event names the workspace has seen. Both sends carry anIdempotency-Key, generated per call or passed asoptions.idempotencyKey, so a retry never records an event twice, andcontacts.listEvents,listAllEventsanditerateEventsread the events of one contact. Sending needscontacts:write, reading the events of a contact needscontacts:readandlistNamesneedsautomations:read, and a key or an app may send 600 events a minute.WEBHOOK_EVENTSgainedautomation.entered,automation.exited,automation.pausedandautomation.webhook, soWebhookEventis wider. Likeform.*, they belong to the whole workspace, have to be named ineventTypes, and never reach an endpoint limited to some addresses or domains.threads.muteandthreads.unmutemute 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-inMUTElabel andis:mutedfinds it inquery, and both methods needthreads:write.- The inbox has tabs: Primary, Promotions, Updates, Social and Forums.
threads.list,listAllanditeratetakecategoryto read one tab,category:promotionsfilters the same way inquery, andthreads.countsreturnscategories, the conversations and unread conversations of the inbox in each tab. threads.listSenderCategories,setSenderCategoryandclearSenderCategorymanage 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 needsthreads:readand every change needsthreads:write, and a key limited to particular addresses or domains can read the choices but not change them.followUpsis new: the reminders on mail you sent.setasks for a thread to come back to the top of the inbox, unread, with the built-inFOLLOW_UPlabel and a push, if nobody replies byremindAt,listreads the reminders still waiting and the ones that ended, andcancelcancels 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.sendand each message ofemails.sendBatchtakefollowUpAt, aDate, an ISO 8601 instant or a duration such asP3D, to set a reminder as the message goes out. Reading needsthreads:readand every change needsthreads:write.outOfOfficeis new: the automatic reply an address sends while its owner is away.setsaves the reply of one address, or of a catch-all with*@example.com, with its message, subject, start, end andcontactsOnly, replacing whatever was saved,listreads every reply saved in the workspace, or the reply of one address withaddress, andclearremoves 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 needssettings:readand every change needssettings:write.- Labels can be applied by AI:
labels.createandlabels.updatetakeaiInstructions, what the label is for in plain words, andaiEnabled, 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.testruns the AI over the newest 20 inbox threads and returns the ones the label would catch, without labelling anything. It needslabels:writeandthreads:read, and spends one AI action. settings.getandsettings.updatecarry three more account settings:inboxTabsshows the inbox as tabs,primaryOnlyAlertskeeps pushes, sounds and desktop notifications to mail sorted into Primary while tabs are on, andphoneFollowUpssends 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 ofemails.sendBatchandtemplates.sendtakestream:transactional, the default, for mail one person is waiting for, orbroadcastfor newsletters. EveryEmailResourcecarries thestreamit went out on, and every copy of a broadcast is on the broadcast lane. sendingis new: it reads the lanes.listStreamsresolves 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 409stream_paused, and app mail keeps going.resumeStreamresumes it, for the workspace owner, and asks an app for a verification code, and reading needsemails:readand resuming needsdomains:write.tools.checkAddresssays whether an address is worth sending to before the first message:valid,risky,invalidorunknown, with the reasons. It checks the syntax, whether the domain takes mail, throwaway domains and shared addresses such asinfo@, and suggests a correction for a likely typo. Withemails:readit adds what the workspace knows: whether it suppressed the address and when it last delivered to it or saw it bounce.deepalso asks whether the mailbox exists, billed per address through pay as you go, and needsemails:send, andtools.checkAddresseschecks 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.listAddOnssays whether it is for sale, what it costs, whether the workspace qualifies and whether it has one,billing.startAddOnCheckoutresolves the link where the person pays, andbilling.cancelAddOnends it at the end of the month already paid for.sending.listDedicatedIpsandsending.getDedicatedIpread each IP with itsstatusandwarmupPercent, since a new IP is warmed up over several weeks. The billing calls needbilling:readandbilling:write, belong to the workspace owner and ask an app for a verification code, and reading the IPs needsdomains:read. domains.getMailAppSettingsreturnssmtpSubmission, the SMTP server on port 587 for apps, devices and libraries that only offer STARTTLS. Itssecurityisstarttls, a new member ofMAIL_APP_SECURITIES, and it takes the same username and app password assmtp.importscan read a live mailbox, as the Migrations page of the app does.discoverMailboxfinds the mail server for an address and says what the mailbox signs in with,checkMailboxsigns in once and lists the folders and how many messages an import would bring, andconnectMailboxstarts copying the mailbox into one address, withcontactsandcalendarsto 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:
statusisparkedandremote.resumeAtsays when. When the password stops working it isneeds-password, andimports.resumesends a new one. These calls needthreads:write, withcontacts:writeandcalendar:writefor those parts,ImportResourcegainssource,remoteandchildren, andImportFormatgainsimapandmicrosoft. calendar.importIcsandcontacts.importVcftake 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,listAllImportsanditerateImportsread the imports of each kind,getImportreads one with its counts and a report of what it left out and why, andundoImportdeletes everything an import added. Reading needscalendar:readorcontacts:read, and the rest needscalendar:writeorcontacts:write.me.listRequests,me.listActivityandme.statslet 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.listWorkspaceRequestsandkeys.statsare still the ones that reach other keys, behindkeys:read.me.statsfillssendsonly when the key holdsemails:readand leaves itnullwithout that scope, and an OAuth access token is refused with 403api_key_only, because calls made with one are not logged.- The request log takes two more filters on
keys.listRequests,keys.listWorkspaceRequests,me.listRequestsand theirlistAll…anditerate…twins.statuseskeeps the calls answered with one of up to 20 exact HTTP status codes, andpathkeeps the calls to one route, whatever the method.pathmatches the route, not the exact path: an id counts as:idand 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:
mapsToalso takesphone,company,jobTitle,websiteandbirthday, saved on the contact card when the sign-up is accepted and only where the card has nothing yet. A single contact now comes withsignUps, its last 20 sign-ups with their answers, for a caller that holdsforms: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:alljoins 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 holdsemails:send.ConsolePermissionincludes it, androles.listPermissionsreturns it withscope: falseunder the newaddressesgroup. 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:managein that member’s role fordomains.deleteanddomains.deleteAddress, as removing one in the app does, and is refused with 403insufficient_authoritywithout it. - The
webhooksmethods 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 holdsaddresses: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 422capability_unsupportedon an endpoint with no allowlists, unlesswebhooks.updategives it some, and on awebhooks.createwithout 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.resendInvitationis refused with 403insufficient_authoritywhen the invitation’s role holds a permission the key or access token does not, the rulemembers.addalready 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.listdocs say how to read a member whosepermissionsholdaddresses:all: they reach every address, and theiraddressesanddomainslist only the grants made to them directly, which may be none. - The
roles.createdocs no longer say a console-only permission is accepted on a role. A key or access token adding one is refused with 403insufficient_authority, as it always was, and theroles.update,members.addandmembers.updatedocs now say so too. - The
roles.listPermissionsdocs no longer sayapi-keys:readandapi-keys:writedecide who may mint keys: API keys stay with the workspace owner whatever a role holds. - The
addresses.listdocs say an app a member connected always reportsunrestricted: false, because it is held to what that member reaches. domains.getLogoreads 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,setLogoCertificateandremoveLogoCertificatechange them, and each says inrecordswhat happened to the DNS record.domains.updatetakesdmarcPolicy, which isnone,quarantineorreject, since inboxes show a logo only atquarantineorreject. EveryDomainResourcecarrieslogoanddmarcPolicy.domains.setAddressPhotoandremoveAddressPhotoset and remove the photo of an address, andDomainAddressResourcecarriesphotoUrl.- New types:
DomainLogo,DomainLogoCertificate,DomainLogoStatusResource,DomainLogoChangeResource,DomainLogoSet,DomainLogoCertificateSet,DomainLogoMarkKind,DomainLogoRecordOutcome,DomainParentDmarc,DomainDmarcPolicyandDomainAddressPhotoOptions, with theDOMAIN_DMARC_POLICIES,DOMAIN_LOGO_MARK_KINDSandDOMAIN_LOGO_RECORD_OUTCOMESsets. formsis 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,listStartersandgetStartermanage the forms, andlistSubmissions,listAllSubmissions,iterateSubmissions,getSubmission,deleteSubmission,deleteSubmissions,approveSubmissionandresendConfirmationmanage what people sent.forms.deleteasks an OAuth access token for a verification code.forms.subscribesigns someone up through a published form and sends no credential. A missing or invalid answer is refused with 422invalid_form_submission, andNextmailApiErrornow carriesfields, the problems that refusal lists, andbody, the parsed error response.- The new scopes are
forms:readandforms:write.approveSubmissionalso needscontacts:write, andresendConfirmationalso needsemails:send, as does a call that makes a form send mail. The webhook eventsform.submittedandform.confirmedcarryFormSubmittedEventDataandFormConfirmedEventData. FORM_STATUSES,FORM_SUBMISSION_STATUSES,FORM_FIELD_TYPES,FORM_STARTER_SLUGS,FORM_CONFIRM_SOURCESand the otherFORM_*sets hold the values and are exported from the package root.appHostis 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.getreads it with its status and the DNS records to publish,setsets or replaces it,verifychecks it straight away, anddeleteremoves it, asking an OAuth access token for a verification code. It needs a paid plan. Reads needdomains:readand changes needdomains:write.brandingis new: the brand of the workspace, the settings on its Customisations page.getreads its mark, its logos, its sign-in photo, its fonts and its sign-in background,updatechanges the fonts and the background,uploadImageuploads one of the four images, andremoveImageremoves one. Reads needsettings:readand changes needsettings:write, and a personal space is refused with 409branding_unavailable.contacts.listCards,listAllCards,iterateCards,getCard,createCard,updateCardanddeleteCardread 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,resetFeedanddeleteFeedmanage secret links that publish the calendar to Google Calendar, Outlook or Apple Calendar, andlistSubscriptions,createSubscription,updateSubscription,refreshSubscriptionanddeleteSubscriptionmanage the calendars you subscribe to by link.calendar.createEvent,updateEvent,deleteEvent,cancelEventandrespondToEventput events on the calendar, call off a meeting and answer an invitation.respondToEventcan propose a new time,declineProposalkeeps the original one, andthreads.getEventreads the event a thread carries. Every event carriessubscriptionId,rdatesandrecurrenceId.domains.getMailAppSettings,listAppPasswords,createAppPassword,updateAppPassword,deleteAppPasswordandsendMailAppSetupput 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.updateMailAppSettingsturns each kind of connection on or off.domains.updatetakessenderName, the name mail from a domain goes out under when the sending address has no name of its own, anddomains.updateAddresstakesdestinationto forward the mail of an address without keeping a copy.domains.listAddressForwards,addAddressForwards,updateAddressForward,deleteAddressForwardandresendAddressForwardConsentmanage where the mail of an address is forwarded, andgetAddressLogin,setAddressLoginanddeleteAddressLoginthe password that signs in as one address alone.knowledgeis 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 areknowledge:readandknowledge:write.emails.compose,emails.rewriteandemails.suggestSubjectwrite with AI as the composer does, and one description toemails.composecan change a whole email: its body, subject, recipients, send time, sender, template and attachments.emails.checkscores a message for spam, phishing and AI writing before it is sent.threads.list,listAllanditeratetakesemanticto match the words of a query by meaning, andaddressto keep the threads delivered to one address.threads.replySuggestionsreads the replies the reading pane suggests under the latest message of a thread.threads.counts,summary,restoreanddeletecount the conversations in each folder, read the AI summary of a thread, take one back out of the Bin and delete one for good, andthreads.listNotes,createNote,updateNote,deleteNoteandreorderNotesread and write its private notes.workspacesandaccountact 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 scopesaccount:readandaccount:write.billingreads 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 scopesbilling:readandbilling:write.dnsConnectionsreads and disconnects the DNS provider accounts connected to the workspace, anddomains.getDns,setDnsZoneandsyncDnsread how the DNS of a domain is set up, choose the zone it goes through and write or repair its records.exportsmakes a zip of the whole workspace,subscriptionsmanages the newsletters and mailing lists the mailbox receives,analyticsreads the numbers behind the Analytics page, andchatsmanages the conversations held with the assistant.templates.listImages,uploadImage,designandredesignreach the image library and the AI designer of the template editor,forms.designandforms.redesigndo the same for sign-up forms, andemails.updatechanges a message that has not gone yet.senders.getandsenders.researchread and look up what is known about the organisation behind a sender,encryptionpublishes and looks up OpenPGP keys,webhooks.listEventsandwebhooks.statsdescribe the events and how deliveries went, andkeys.statsreads what the keys did.members.grantDomainandmembers.revokeDomaingive a member a whole domain and take it back,files.revokeAllLinksmakes a file private at once,broadcasts.analyticscompares the broadcasts sent inside a window, andbroadcasts.statstakesdaysorminutesto report what happened inside one.tools.checkDmarc,checkBimi,checkDeliverability,readVerificationCodeandestimateCostare the free checkers of the website, andsupport.sendwrites to the people who build Nextmail. None of them needs a scope.providerImportsmoves an account across from SendGrid, Postmark, Mailgun and Mailchimp as well as Resend, andIMPORT_FORMATS, now exported, includesproton.settings.getandsettings.updatereach every setting the app has:@domainsets a whole domain,nulllets a scope inherit again, andworkspaceNamerenames the workspace.drafts.createtakesforwardOfto forward a message, andtemplates.publishtakesexpectedVersion.- A message whose recipients all bounced reads
bounced, and one that bounced for some of them readspartial, where both used to readsent. The messagesthreads.getreturns carrysentWith,forwardedFromandverificationCode. drafts.updatekeeps the attachments and the forward of a draft, and thesecurity.verifyStepUpdocs 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.listAttachmentsresolvesTempAttachmentResourcein place ofAttachmentResource:attachmentId,filename,mimeType,sizein decoded bytes,body, always an empty string, andheaders, always empty. No route on a disposable inbox serves the bytes, so thecontentTypeandcontentthe old type declared never arrived.tempMail.extendresolvesExtendedTempInboxResource, the inbox plus the newtokenthe response always carried. That token holds the later expiry while the old one keeps its old expiry, so use it from then on.- The
tempMailmethod 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:
TempAttachmentResourceandExtendedTempInboxResource.
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
accessTokenin place ofapiKey: 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()readsNEXTMAIL_ACCESS_TOKEN, andisAccessTokenchecks the shape the API accepts. me.getandme.pingdescribe an access token as well as a key. A token answersobject: 'oauth_token'with aclientId, and both answers carrykind,apiKeyoroauth, fromCREDENTIAL_KINDS. Code that readidorkeyIdas a plain string narrows onobjectorkindfirst.security.stepUpStatus,beginStepUpandverifyStepUphandle 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 400step_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.isStepUpRequiredis true when a request was refused withstep_up_required. Verify a code, then replay the request once.threads.getreturnsdeliveredTo, the workspace address the thread belongs to, or null when none was recorded.- A
raw.requestpath must begin with a single/and stay on the origin ofbaseUrl, or the call throws before anything is sent. - A credential is never attached to a request over plain
http:to another machine.localhost, any127.x.x.xaddress and::1still work, and abaseUrlon0.0.0.0or[::]throws and names the address to use instead. - New types:
AccessTokenProvider,ApiKeySelfResource,OauthTokenSelfResource,ApiKeyPingResource,OauthTokenPingResource,CredentialKind,StepUpStatusResource,StepUpChallengeResource,StepUpVerifiedResource,StepUpBegin,StepUpVerify,StepUpMethodandStepUpErrorCode.
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.createadds a domain and returns it with every DNS record to publish.domains.verifychecks its DNS straight away, anddomains.deleteremoves it and returnsleftBehind, the records that have to be removed by hand. The last domain in a workspace cannot be removed through the API and throws 409last_domain.domains.get,update,createandverifyreturnrecords, each DNS record with itstype,name,value,priority,purposeandstatus(found,missing, or null before the first check), anddmarc, the DMARC reading, or null until the domain is verified.DOMAIN_RECORD_STATUSESandDOMAIN_DMARC_STAGEShold the values.domains.updatetakescatchAllbesidetrackingHostandstorageHost.- The addresses on a domain have their own methods.
domains.listAddresses,listAllAddressesanditerateAddressespage through them, anddomains.createAddress,getAddress,updateAddress(rename it, or turn it off and on) anddeleteAddressmanage one. Reads needdomains:readand changes needdomains:write. audiences.listContacts,listAllContactsanditerateContactstakestatuses, which keeps only thesubscribedmembers, only theunsubscribedones, or both.AUDIENCE_MEMBER_STATUSESholds the values.- Every
FileResourcecarriesvisibility,publicwhile a download link to the file works andprivateotherwise, andpublicUrl, a working public link or null. Uploads start private.FILE_VISIBILITIESholds the values. - New types:
DomainCreate,DomainRecordResource,DomainRecordStatus,DomainDmarcResource,DomainDmarcStage,DomainAddressResource,DomainAddressCreate,DomainAddressPatch,DeletedDomainResource,DeletedDomainAddressResource,AudienceMemberStatusandFileVisibility.
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,listAllRecipientsanditerateRecipientslist everybody a broadcast went to, sorted by address, with each copy'sstatus,sentAt,deliveredAt,bouncedAt,complainedAt,opens,clicksandunsubscribedAt.filterkeeps one group (BROADCAST_RECIPIENT_FILTERS:pending,sent,delivered,opened,not_opened,clicked,bounced,complained,failed,unsubscribed) andqsearches the address and name.broadcasts.getRecipientreturns one copy with itssubject,htmlandtextexactly as that person got it, andbroadcasts.statsreturns the totals and a series pergrain. All five needemails:read. New types:BroadcastRecipientResource,BroadcastRecipientContentResource,BroadcastRecipientListOptions,BroadcastRecipientFilter,BroadcastStatsResource,BroadcastStatsOptions,BroadcastStatsTotalsandBroadcastStatsBucket.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 includesfiles:read. Reading an email withthreads:readstill reads the attachments on it.files.createLinkpublishes a file at a public download link that opens with no sign-in, on the files host ofoptions.domainwhen that domain has one, such asfiles.acme.com.files.revokeLinkstops a link from working, including in mail that already went out.files.list,files.listAllandfiles.iteratetakedirection(inbound,outboundoruploaded),address(the files of one address), andsinceanduntil(aDateor an ISO 8601 string) besideq,kindandsort.- Two webhook events:
file.uploadedwhen a file is put on the Files page, andfile.deletedwhen one is deleted. Their data isFileEventData. They are not in the default set, so an endpoint names them ineventTypes, and an endpoint limited to some addresses hears only about the files of those addresses. - Two new types:
FileLinkCreateOptionsandFileEventData. - 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_unsupportedbefore. A broadcast'sfrommust still be an address the key may send as.broadcasts.list,get,stats, the recipient methods andcancelreach only the broadcasts sent from an address or domain the key holds, and any other id throwsbroadcast_not_found.contacts.listPeoplereturns the saved contacts alone for such a key.contacts.listThreads,contacts.activity,contacts.blockandcontacts.unblockstill refuse it with 422capability_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 needfiles:readorfiles:writewhere they neededthreads:readorthreads:write. Keys, roles and app grants that heldthreads:readorthreads:writewere given the matchingfiles:*scope on the server, so the change only shows on a key created afterwards without them, which is refused with 403insufficient_scope.
const stats = await nextmail.broadcasts.stats('brd_5a8c1e3f7b2d94a06c8e1f3b')const bounced = await nextmail.broadcasts.listAllRecipients('brd_5a8c1e3f7b2d94a06c8e1f3b', { filter: 'bounced' })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.uploadstores a file on the Files page, the way its Upload button does, and resolves with the newFileResource. The bytes go as aBlob,ArrayBufferorUint8Array, with the name inoptions.filenameand the type inoptions.contentTypeor theBlob’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 needsthreads:write. An upload may take up to 10 minutes, or the clienttimeoutMswhen that is longer, andoptions.timeoutMssets its own limit.files.deleteManydeletes up to 100 files in one call and reports the rest instead of failing:deletedandmissinghold ids, andkeptnames each file it kept with itsusageand areason.files.statsreturns 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.FileResourcegainedusageanddeletable.deletableis true only for an upload that nothing depends on. Otherwiseusagesays what keeps the file:receivedorsentfor one that came in or went out on a message,linkedfor an upload that went out as a download link that still works, andscheduledfor one attached to a message that has not gone out yet.FILE_USAGESis exported from the package root besideFILE_DIRECTIONS. Ten types are new:FileUsage,FileUploadOptions,FileBatchDeleteResource,FileKept,FileStatsResource,FileCount,FileTypeCount,FileDayCount,FileAddressCountandFileLinkCount.RequestOptionsgainedtimeoutMs, a limit for one call in place of the client’s.FILE_DIRECTIONSgainedUPLOADED('uploaded'), and files added on the Files page come back fromfiles.listandfiles.getwithdirection: 'uploaded'.FileDirectionis wider, so aswitchor aRecordthat covers every member has to coveruploaded.audiences.growthcounts unsubscribes. Each bucket gainedunsubscribed, the contacts that left the audience through a broadcast unsubscribe link in that bucket. Each series gainedsubscribed, how many of its contacts are still subscribed, andunsubscribedfor the window.totalsgainedsubscribed, the contacts still subscribed to at least one of the audiences read, andunsubscribed. A bucket now appears when somebody joined or unsubscribed in it.files.deletedeletes 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 409file_in_use, where it used to be deleted. Deleting is no longer the workspace owner’s alone: any key or app holdingthreads:writemay delete, so the 403owner_onlyis gone. The change is on the server, so 0.0.2 sees it too.
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.audiencesis new: named lists of contacts, withlist,get,create,update,delete,listContacts,addContactandremoveContact. Every workspace has one default audience, the one whosebuiltinisdefault, which holds every contact and cannot be deleted.nextmail.contactscan write.create,updateanddeletejoinlistandget, and each one takes the email address as the id.deletealso hides the address, and an address that was not in the book resolves withwasSavedfalse rather than a 404.nextmail.domains.update()sets or clears a domain's own tracking name and files name, andDomainResource.trackingandDomainResource.storagereport 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.domainAllowlistnarrows a key to whole domains, beside the individual addresses inaddressAllowlist. 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, andattachmentDeliverypicks 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.downloadedfires 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:readandkeys:manage. - Twelve fields that were loose strings now have nine named types, among them
RecipientKind,ContactSource,RuleMatchModeandWebhookDeliveryStatus. They accept and return the same strings as before. nextmail.importsis 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.importFilescreates the import, uploads each file in parts and starts it in one call, andcreate,uploadChunk,uploadState,start,get,list,listFailures,cancelanddeleteUploadare 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.providerImportsis new: move a Resend account across.inspectreads a full-access key and returns what an import would bring without storing anything, andcreateruns 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.webhookscan look into a delivery and send it again.getDeliveryreturns one attempt with the body that was sent, what your server answered and every other attempt of the same event, andreplayDeliverysends that event once more with its original id, one event at a time, refusing withretry_in_progresswhile an automatic retry of it is being sent and withreplay_in_progresswhile another replay of it is.listAllDeliveriesanditerateDeliverieswalk the whole delivery log, andWebhookDeliveryResourcegainedeventIdandnextAttemptAt.- 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
idandcreatedAt. 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.listandproviderImports.listreturn aPagewithlistAllanditeratebeside them.tempMail.listMessagesgainedcursor,hasMoreandnextCursor, so an inbox past 50 messages shows them all.contacts.listAllandcontacts.iterateare new. nextmail.labelsdoes everything the app does with labels.listColorsreturns the palette of fourteen solids and seven gradients, a colour can be a hex value or a gradient token such asgradient:sunset,updatetakes a partial patch in whichcolor: nullclears the colour, and every label carriesthreadCount,createdAtandupdatedAt.nextmail.audiencesdoes everything the audience page does.addContactsandremoveContactschange up to 200 memberships in one call,importContactscreates contacts and adds them to a list up to 500 rows at a time,emptytakes everybody out of a list at once,growthreads how many contacts joined each list over a window, andcontacts.setAudiencessays exactly which lists one contact is in.audiences.listContacts,listAllContactsanditerateContactstakeq,sourceandsort, the search, filter and five orders of the audience page, and every row is anAudienceContactResourcecarryingaddedAt, the moment the contact joined that audience.AudienceResourcecarriescontactCount, counted at the moment of the read, andlastContactAt, when the most recent contact joined the audience.- A key limited to particular addresses or domains reads exactly what they reach.
emails.listfilters before it cuts a page, so every page but the last is full,rules.listandrules.getshow only the rules that can act on mail delivered to those addresses, andrules.listRunsonly their runs, whilerules.create,rules.update,rules.deleteandrules.reorderchange only rules that act on nothing but mail to those addresses. A recurring event is expanded in full however itsBYDAYis written and however long ago the series began. These are changes on the server, so every caller sees them. nextmail.contactsdoes everything the Contacts page does:listPeoplewith the people seen in mail, thread counts and a blocked filter,save,deleteMany,setPhotoandremovePhoto,blockandunblock,listThreadsandactivity.listtakesqandsource, andContactResourcegainedphotoUrl.threads.list,listAllanditeratetakesort,dateFrom,dateToandfromContacts, 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.suppressionsis new: the Blocked addresses screen, withlist,listAll,iterate,get,addandremove, andSUPPRESSION_REASONSnaming the three reasons.nextmail.filesis new: the Files page, withlist,listAll,iterate,get,download,listLinks,listAllLinks,iterateLinksanddelete.members.listInvitations,listAllInvitations,iterateInvitations,resendInvitationandrevokeInvitationfollow an invitation up afteradd.InvitationResourcecarriesexpired,lastSentAtanddelivered, which is null when no outcome was recorded for the last send.nextmail.keysis new: the API keys page from code.list,get,create,update,rotate,revokeanddeletemanage keys,createandrotatereturn a secret once, andlistRequests,listActivity,listWorkspaceRequestsandlistWorkspaceActivityread the request log and the audit log with the console’s filters. Reading needs the newkeys:readscope and every change the newkeys:managescope, and a key never makes or reaches a key wider than itself.nextmail.broadcastsis new: send one message to everybody in one or more audiences, a separate copy for each person.sendanswers at once and the sending runs in the background,getreadsstatusand livecounts,list,listAllanditeratepage through past broadcasts,previewcounts who a send would reach, andcancelstops the copies that have not gone.subject,htmlandtexttake 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.sendcarries anIdempotency-Key, so a retry never sends twice.EmailResourcegainedbroadcastId, andemails.list,listAllanditeratetakebroadcastIdto list the copies of one broadcast.AudienceContactResourcegainedunsubscribedAt, set when the contact unsubscribed from a broadcast to that audience.nextmail.webhooksreads its logs the way the console does.listDeliveriestakesstatus,sinceanduntil,listWorkspaceDeliveriesreads every endpoint at once, andlistActivityandlistWorkspaceActivityread who changed what.WebhookDeliveryResourcegainedendpointId.nextmail.templatesdoes everything the editor does:duplicate,replaceContent,restoreVersion,deleteVersion,getVersion,listStarters,getStarterandlistFonts,createfrom astarter,renderfor a body that is not stored anywhere, andgetAnalyticsandlistSendsfor how a template performed.listtakessearchandsort, andupdatetakesslug.settings.getandsettings.updatetake anaddressoption, an address or*@domainfor a catch-all, to read and set the signature and tracking of that address, andSettingsResourcegainedaddress.contacts.get,createandupdateresolve withContactDetailResource, the contact and the audiences it is in, andContactResourcegainedcreatedAtandupdatedAt.- Webhooks can be narrowed to addresses and domains of their own with
addressAllowlistanddomainAllowlist, 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:
MemberAddtakesdomainIds, andMemberResourceandInvitationResourcecarrydomains.MemberResource.isOwnermarks the workspace owner. - Fourteen more sets are exported from the package root, among them
BROADCAST_STATUSES,SUPPRESSION_REASONS,THREAD_SORTSandWEBHOOK_REPLAY_ERROR_CODES, and everyimportsandproviderImportstype can be imported by name. tempMail.listAllMessagesanditerateMessagesare new,RequestOptionsgainedraw,contentTypeandbinary, 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.
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.listreturnsPage<ContactResource>instead of an array. Read the rows frompage.itemsand followpage.nextCursorwhilepage.hasMoreis true. The address book is unbounded, and the old array held one call’s rows, 50 unlesslimitasked for up to 200, with no way to reach the rest.API_SCOPES,ERROR_TYPES,MESSAGE_ENCRYPTION_FORMATS,RULE_ACTIONS,RULE_FIELDS,RULE_OPERATORSandWEBHOOK_EVENTSare keyed objects rather than arrays, so every member has a name to call it by. The values they held are unchanged.contacts.listsorts 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.listDeliveriesreturnsPage<WebhookDeliveryResource>instead of an array and takeslimitandcursor. It used to stop at the 50 most recent attempts with no way to reach older ones. Read the rows frompage.itemsand followpage.nextCursorwhilepage.hasMoreis true, or collect every attempt withlistAllDeliveries.emails.listEvents,tracking.list,tracking.listOpensandtracking.listClicksreturn aPageinstead of an array and takelimitandcursor. 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. Readpage.itemsand followpage.nextCursor, or uselistAllEvents,listAll,listAllOpensandlistAllClicks.labels.listreturnsPage<LabelResource>sorted by name instead of an array, andcreateandupdateresolve with the wholeLabelResource, soSavedLabelResourceis gone. On the server,threads.updaterefuses anaddLabelIdsid that names no label with a 422label_not_foundinstead 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.listandroles.listreturn aPageinstead of an array and takelimitandcursor, withlistAllanditeratebeside each.addresses.listreturnsAddressBookPage, the book it returned before plushasMoreandnextCursor, so the addresses come a page at a time, andaddresses.listAllcollects the whole book. Readpage.itemsand followpage.nextCursorwhilepage.hasMoreis true, or uselistAll.templates.listVersionsandcalendar.listEventsreturn aPageinstead of an array and takelimitandcursor, withlistAllVersionsanditerateVersions, andlistAllEventsanditerateEvents, beside them. Send the samefrom,toandtimezonewith everycursorofcalendar.listEvents.- The cursors of
members.list,roles.list,domains.list,webhooks.list,addresses.list,labels.list,rules.listandtemplates.listare opaque rather than the id of the last row, so a row deleted between pages no longer makes the next page a 400invalid_cursor. PassnextCursorback as it came, since a cursor built from an id is refused. members.addsends an invitation and resolves withInvitationResource(202) instead ofMemberResource(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 422member_is_ownerwhere 0.0.1 changed their role, and inviting the same address again within ten minutes is a 409invitation_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_unsupportedonmembers.add,update,remove,grantAddressandrevokeAddress, and onroles.create,updateanddelete. The change is on the server, so 0.0.1 sees it too. - Every workspace cap answers 422
workspace_limit_reached, wherewebhooks.create,templates.create,rules.createandroles.createansweredwebhook_limit_reached,template_limit_reached,rule_limit_reachedandrole_limit_reached. The change is on the server, so 0.0.1 sees it too. - The signature and tracking belong to each address.
settings.updatewithout anaddressrefusessignature,openEmailSignature,trackOpensandtrackClickswith 422address_required. The change is on the server, so 0.0.1 sees it too. AttachmentInputis the unionInlineAttachmentInput | StoredAttachmentInput. Passing{ filename, content }works as before, but code that readsfilenameorcontentfrom a value typedAttachmentInputhas to narrow it first.TemplateContent.documentis aTemplateDocument, abodyof typed blocks, rather thanRecord<string, unknown>.TemplateDocument,TemplateBlockandTemplateBlockKindare exported to build one in code.TrackingStatsResource.hitsKnownFromis gone, andEmailSourceno longer includesqueue.API_SCOPESgained six scopes andWEBHOOK_EVENTSten events, soApiScopeandWebhookEventare wider. Aswitchor aRecordthat covers every member has to cover the new ones.
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
listAllanditerate. - Two error classes, and retries that cannot duplicate a send.
- ESM and CommonJS, on Node 20+, Bun, Deno and Cloudflare Workers.