Repository navigation
Releases: mailtea-app/mailtea-node
Release list
v0.22.0
- Changed (API):
posts.send, andposts.createwithsend: true, send to
the whole audience. They used to stop at 10,000 recipients without saying
so. One send can reach at most 25,000; a larger audience is refused with a
422audience_too_largethat carriesaudience_countand
max_recipients, and nothing is sent or scheduled. Onposts.createthe
error body carries the draft'sid. A send also answers 409post_changed
when the post was scheduled or edited elsewhere (for example in Mailtea
Studio) while the request was preparing it, instead of sending it anyway.
No package change: this is the API's behaviour once it is deployed.
Released from mailtea-app/mailtea@1d368b5.
v0.21.0
- Changed (API):
posts.send, andposts.createwithsend: true, now fail
with a 403system_domain_recipient_restricted(withrestriction) when
the team has no verified sending domain and the audience includes anyone
outside the team, or the post sends from the built-in*.mailtea.email
address. Nothing is sent or scheduled and the post stays a draft; on
posts.createthe error body carries the draft'sid. The send used to be
accepted and fail a moment later. A broadcast (kind: "broadcast") is never
published to the website, however it is sent. No package change: this is
the API's behaviour once it is deployed.
Released from mailtea-app/mailtea@68b43c2.
v0.20.0
- Changed (API):
contacts.list({ search })with several whole addresses
separated by commas, spaces or line breaks now returns exactly those
contacts (up to 200), the same as Mailtea Studio and MCP. A list with an
entry that is not a whole address is refused with a 400 naming it. Part of
an address still matches as before. Thesearchfield documents this. - Added:
"domain_managed"toDomainRefusalCode.domains.updateon the
Mailtea-managed domain (<slug>.mailtea.email) is now refused with it (400)
instead of changing a domain Mailtea runs. Needs the API deployed with this
change. - Added:
CreateDomainClaimInput.purpose("email","site"or"both",
default"email") andDomainClaim.purpose. A claim now creates the domain
with that purpose, so a website-only domain no longer needs adomains.update
afterwards, and a"site"claim gets no sending identity. Claims opened
before this change report"email". Needs the API deployed with this change. - Added:
Post.segment_id,CreatePostInput.segment_idand
UpdatePostInput.segment_id(nullclears it). A post with a segment goes
to that segment's active contacts instead of all active contacts; the
segment must be in the post's publication, or the request is refused with a
422. The segment is resolved when the post is sent, and a send to a segment
that cannot be resolved or matches nobody is refused rather than widened to
the whole list. A segment that a draft, scheduled or sending post targets
cannot be deleted:segments.deletefails with a 409segment_in_use, so
point those posts at another segment or clear theirsegment_idfirst. Deleting one used to send those posts to everyone. - Added:
Segment.inactive_days,CreateSegmentInput.inactive_daysand
UpdateSegmentInput.inactive_days(nullclears it): contacts with no open
or click in the last N days (1 to 3650), counting contacts who never
engaged. It selects the silent cohort, not engaged readers. Engagement
tracking is not backfilled, so contacts with no recorded engagement count
as inactive. Both need the API deployed with this change; this release adds
the fields to the types.
Released from mailtea-app/mailtea@1ea0e1b.
v0.19.0
- Changed:
DkimStatusincludes"revoked", which the API already returns in
dkim_statusfor a domain Mailtea revoked after abuse. Such a domain can't
send, and saving or re-adding it keeps it revoked. Code that switches over
DkimStatuswithout a default should handle the new value. - Added:
TemplateVersion.from,TemplateVersion.reply_toand
TemplateVersion.sender_recorded. Template history now records the sender:
an update that changes onlyfromorreply_torecords a version (or folds
into the open one, like any edit), andtemplates.restoreVersionbrings the
version's From and Reply-To back.sender_recordedsays what anull
means. When it is true the version had no From or Reply-To, and restoring it
clears them. When it is false the version was recorded before this change,
and restoring it leaves the current From and Reply-To alone. The server
change reaches every SDK version on deploy; this release only adds the
fields to the type.
Released from mailtea-app/mailtea@ff8e7ef.
v0.18.0
-
Added: optimistic-concurrency tokens for editing over an API that may also
be edited in Mailtea Studio or by another agent.Template.revisionmoves
on every content-changing write; pass it back asbase_revisionon
templates.updateandtemplates.publishto get a 409stale_write
(current_revisionon the error) instead of silently overwriting someone
else's edit.automations.updatetakesbase_versionthe same way for
graph writes (steps), answering 409stale_version.Post.updated_atis
now returned byposts.get/posts.list/posts.update; pass it back as
base_updated_atonposts.updatefor a 409stale_write
(current_updated_aton the error). Every token is optional: omit it for
today's unconditional write. -
Added:
Post.fromandPost.reply_to, the From and Reply-To set on a post
(nullwhen the named sender or the publication default decides). The API
now keeps thefromandreply_toyou pass toposts.createand
posts.updateinstead of dropping them. -
Changed:
nameonposts.create/posts.updateis only the post's internal
name and no longer overwrites the subject.posts.updatechanges only the
fields you pass, and""clearsname,fromorreply_to. -
Note: a
fromthat is not on one of the publication's verified sending
domains is now refused with a 422 (the samereason,codeanddomainas
emails.send), andreply_tomust be a valid address. -
Docs:
automations.activatelists the two cloud-onlyno_verified_sender
reasons,CUSTOM_DOMAIN_REQUIREDand the newBUILT_IN_SENDER(a step sends
from the built-in{slug}.mailtea.emailaddress). No runtime change. -
Changed:
domains.list({ region })accepts any string as well as the catalog
regions, because the API now also accepts the deployment's default region,
which a domain with no stored region reports (for exampleus-east-1in local
development or on a self-hosted install). Type-only; no runtime change. -
Docs: the
assetsresource no longer says SVG is refused. It is accepted and
served under a sandboxing Content-Security-Policy; use PNG or JPEG for email,
because Gmail and Outlook do not show SVG. -
Breaking:
posts.createwithtemplate_idnow HTML-escapes thevariables
you pass, the same as every other send. HTML passed in a{{key}}value now
arrives as visible text, and a value you escaped yourself arrives
double-escaped. Put{{{key}}}in the template where a value is meant to be
raw HTML. Variables are now filled in both the{{key}}and Visual Email
Designer{key}forms. A declared variable you do not pass stays in the
post with itsfallback_value, so the broadcast gives each recipient their
own value or that fallback, and undeclared tokens like
{{contact.first_name}}are left for the broadcast too. The post keeps the
template's published page style, is wrapped in that page, and has its
show-if blocks decided per recipient when it is sent. Before, only the
variables you passed were replaced, raw, and only in{{key}}form. It uses
the template's published version; Mailtea Studio's "Use template" starts
from the latest saved design instead. -
Changed:
templates.updateandtemplates.restoreVersionno longer move a
published template back to draft. The template keeps its published status,
and automations and the API keep sending its published version until
templates.publishis called again. The template'sfromandreply_to
are part of the published version too, so a new sender or reply-to address
reaches sends only after the next publish.templates.unpublishis now the
only way to stop a published template sending, short of deleting it, and it
drops the stored published version so the next publish starts from the
current content. -
Added:
has_unpublished_versionsonTemplateandTemplateListItem. True
only when the template is published and its saved content (From, Reply-To
and the style profile included) differs from the published version. -
Added:
TemplateVersion.is_published: true for the one entry automations
and the API are sending now.is_currentis now described as what it is:
the entry that matches the working copy (the saved design being edited), not
necessarily what is sending.is_publishedis false on every entry of a
draft, and on a template published before the field existed until it is
published again. -
Changed:
templates.updatereturnsUpdatedTemplate: the template plus the
PATCH reply'sunpublishedand, when the edit is not live yet,message. -
Changed:
RestoredTemplateVersion.unpublishedand the PATCH reply's
unpublishedare kept for compatibility and are now alwaysfalse. Check
has_unpublished_versions(or the reply'smessage) instead. -
Changed (API behavior): a template variable's
fallback_valuecan no longer
contain{or}. Creating a template with one, or changing a fallback
to one on update, is a 400 ("Fallbacks can't contain { or }."). A value
the template already stores is accepted unchanged, so a template saved
before the rule keeps saving. Inline chip fallbacks such as
{first_name|Mom & Pop}now render as written instead of double-escaped. -
Added:
AutomationValidationIssue.pre_existing, set by the API when the
version the automation last ran on already had the same problem (same code,
step and path). -
Changed (API behavior): saving an active automation is refused only when the
edit adds an error the live version does not already have. The 422
active_graph_invalidreply'sissueslists just those new problems.
Before, any error refused the save, even one the live version already had.
Starting refuses every error as before, except anunknown_step_refat a
config.*path or a triggermissing_branchthat the version the
automation last ran on already had, so pausing and starting an unchanged
automation keeps working. Issues the last live version already had come back
withpre_existing: true. -
Added:
AutomationValidationIssue.field, what a rule reads (the rule's
field, or the path in a{"var": ...}value, e.g.
steps.welcome.opened) when the issue is about one. -
Changed (API behavior): two issues are the same problem when their code and
step match, and theirfieldor, when there is none, theirpath. Moving
a rule, by removing a rule beside it or putting it in a group, no longer
makes a problem the live version already had look new. An error is
pre_existingonly if the live version had an error there, not a warning. -
Changed (API behavior):
validate_onlyon an active automation answers the
way the save would. A trigger change is a 422trigger_locked_while_active,
a change that adds a problem is a 422active_graph_invalidlisting only
the new problems, and otherwise issues come back withpre_existingmarked
against the version live now. Before, it returned every issue unmarked. -
Changed (API behavior): changing the trigger (its type or key) of an active
automation is now refused with 422trigger_locked_while_active. Pause it
first; draft and paused automations can still change their trigger. Before,
the change was accepted. -
Added (API behavior): new validation rules. A trigger with nothing after it
is amissing_brancherror atbranches.next. A rule or{"var": ...}
value that readssteps.<key>.*for a step that isn't in the automation is
anunknown_step_referror at thatconfig.*path, or a warning when the
{"var": ...}has adefault. A rule or value that reads
event.properties.*when the automation does not start from an app event is
the new warningevent_field_without_event_trigger. -
Changed:
SendEmailInput.subjectis optional when you send atemplate,
andfrom/sender_idmay both be left out with one. The API then uses
the template's published subject, and its sender is chosen the way an
automation step chooses one (the publication's default sender, then the
template's own From). Without a template both are still required. Batch
items are unchanged:BatchEmailItemInputstill requiresfromand
subject. -
Behaviour (API, no SDK change needed): a template send now fills the
template's variables into the subject with the same values and fallbacks as
the body. A template built in the Visual Email Designer is delivered inside
its designed page background, card and font. Raw HTML templates are sent
exactly as stored.
Released from mailtea-app/mailtea@acea165.
v0.15.0
- Added: test mode.
apiKeys.create({ mode: "test" })mints a test key
(prefixedmt_test_) whose sends are validated, recorded and webhook-emitting
but never delivered, so CI can run against production Mailtea with your real
code and your real webhook handler. A test key is not a data sandbox — it
reads and writes your real contacts, templates, senders and webhooks. Only
delivery is simulated. - Added:
modeonListEmailsParams, soemails.list({ mode: "test" })reads
test-mode mail. There is no mixed view: a test key reads only test emails and
a live key only live ones, and asking for the mode your key is not in is a 400
rather than an empty page. - Added:
modeonEmailListItem,RetrievedEmail,CreatedApiKeyand
ApiKeyListItem, plus theEmailModetype — an open union like
EmailStatus, so a value added server-side still types. - Note:
modeis never accepted on a send. The key decides. - Reserved recipients on
test.mailtea.emailforce an outcome:delivered@,
bounced@,complained@,delayed@,failed@. The firsttorecipient
decides; anything else is delivered. - Added:
MailteaError.reason— the API's second machine-readable
discriminator, alongsidecode. Several routes answer withreasonand no
code(mode_not_available,test_recipient_in_live_mode,
test_mode_daily_cap), and it was being dropped when the response was
parsed, leaving those callers to string-match the message.
Released from mailtea-app/mailtea@8c362fb.
v0.14.0
- Added:
receiving_identity_statuson domain responses (create, get, update,
verify).pending,verified,failed, ornullwhen the domain is not
registered to receive mail yet. Point anMXat Mailtea only once it reads
verified; it never gatesstatus. - Changed:
automations.activatedocuments theno_verified_senderrefusal.
A 422 with that code means asend_emailstep has no sender it can send
from;reason(NO_SENDER,DOMAIN_NOT_VERIFIED,WRONG_PURPOSE,
DKIM_NOT_VERIFIEDorINVALID_FROM) says which, andsteps[]names every
blocking step. Previously onlyautomation_invalidwas documented. - Added:
domains.updatetakestracking_subdomain: nullto remove a tracking
subdomain. The domain's links go back to being served from the Mailtea host.
Links in mail you have already sent point at the old hostname and stop
resolving — there is no way to reinstate them. An empty string is not the same
thing: it is refused withtracking_subdomain_invalid.nullis accepted on
update only;CreateDomainInput.tracking_subdomainstays astring. - Changed: the
MXrow inrecordsnow reports what the last verify found,
instead of readingpendingon every request but the verify itself. A domain
nobody has verified readsnot_started.
Released from mailtea-app/mailtea@fed0718.
v0.13.0
- Added:
mailtea.domains.claims—create,get,verifyandcancel. When
adding a domain is refused with codedomain_held_elsewhere, another
publication holds the host; publish one TXT record to prove you control its
DNS and the domain moves to you. Verifying before the record has propagated
is safe: the claim stays pending with the same record. - Added:
Domaincarriesregion,tls,tracking_subdomainand
released_at. A domain's region decides where its mail is sent from and is
fixed at creation;tls: "enforced"bounces rather than delivering in the
clear; a tracking subdomain serves opens and clicks from your own domain;
released_atis set when another publication claimed the host, and a
released domain cannot send whatever itsstatussays. - Added:
domains.createtakesregion,tlsandtracking_subdomain;
domains.updatetakes the two that are mutable;domains.listtakesregion
andstatusfilters. - Added:
DomainRefusalCodeis exported, so a refusal is matched oncode
rather than on prose. - Added:
TrackingDomain.attached— whether the host is registered on our edge,
which is what makes a certificate exist for it. A verified CNAME with
attached: falsemeans links are still served from the platform host. - Changed: each row in
recordsnow says what it is FOR inrecord
(Ownership,DKIM,SPF,MX,Return-Path,Tracking), carries
ttl: "Auto", and reports its OWN status rather than the domain's — including
not_startedwhere nothing has checked the record yet.typestill holds the
DNS type andpurposeis unchanged. - Added:
contacts.setPropertyValues(contactId, { publication_id, values })and
contacts.listPropertyValues(contactId, { publication_id }). These write and
read the per-contact values behind{{contact.<key>}}merge tags. Defining a
property only created the field — until now there was no way from the SDK to
put a value on a contact, so a script could definefirst_nameand never set
it. Each entry takes eitherproperty_idorkey(not both); an empty
valueclears the property, and itsfallback_valueapplies again.
Released from mailtea-app/mailtea@1dff0ba.
v0.12.0
- Added:
baseUrlfalls back to theMAILTEA_API_BASE_URLenvironment
variable before defaulting tohttps://api.mailtea.app. Point a self-hosted
or local instance at the SDK without threading an option through your code.
An explicitbaseUrlstill wins. This closes a gap the other clients did not
have — the Python SDK, the CLI and the MCP server have always read it. - Fixed: the global
fetchis now bound before it is stored, so the SDK works
on Cloudflare Workers without a wrapper. The stored reference is called bare,
which strips the globalthis; Node and browsers tolerate that, the Workers
runtime answersTypeError: Illegal invocation. Workers users no longer need
to passfetch: globalThis.fetch.bind(globalThis). Afetchyou supply is
still used exactly as given.
Released from mailtea-app/mailtea@a0172ed.
v0.11.0
- Documented: the API now enforces your plan's analytics retention window on
from_date. It is clamped to 30 days on most plans and 90 on Scale and
Enterprise; a value reaching further back returns data from the start of that
window rather than an error, and omitting it returns the window rather than
all time. - BREAKING (types only) —
EmailAnalytics.from_dateis nowstring, not
string | null. The server always reports the window it used, so the field
can no longer be null. UnderstrictNullChecksa null comparison against it
is now a compile error; delete the null branch. No runtime behaviour changes,
and reading the field is unaffected. - Added:
EmailListResponse.from_date— the list response now reports the
window used, the same way analytics does. That is how a clamped request stays
visible rather than silently returning less. - Changed:
EmailListParams.from_date,EmailAnalyticsParams.from_dateand
EmailAnalytics.from_datecarry it in their doc comments.
Released from mailtea-app/mailtea@c311c79.