Skip to content

Releases: mailtea-app/mailtea-node

v0.22.0

Choose a tag to compare

@davepoon davepoon released this 14 Oct 12:12
  • Changed (API): posts.send, and posts.create with send: 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
    422 audience_too_large that carries audience_count and
    max_recipients, and nothing is sent or scheduled. On posts.create the
    error body carries the draft's id. A send also answers 409 post_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

Choose a tag to compare

@davepoon davepoon released this 03 Oct 06:47
  • Changed (API): posts.send, and posts.create with send: true, now fail
    with a 403 system_domain_recipient_restricted (with restriction) 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.create the error body carries the draft's id. 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

Choose a tag to compare

@davepoon davepoon released this 01 Oct 09:04
  • 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. The search field documents this.
  • Added: "domain_managed" to DomainRefusalCode. domains.update on 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") and DomainClaim.purpose. A claim now creates the domain
    with that purpose, so a website-only domain no longer needs a domains.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_id and
    UpdatePostInput.segment_id (null clears 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.delete fails with a 409 segment_in_use, so
    point those posts at another segment or clear their segment_id first. Deleting one used to send those posts to everyone.
  • Added: Segment.inactive_days, CreateSegmentInput.inactive_days and
    UpdateSegmentInput.inactive_days (null clears 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

Choose a tag to compare

@davepoon davepoon released this 29 Sep 02:33
  • Changed: DkimStatus includes "revoked", which the API already returns in
    dkim_status for 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
    DkimStatus without a default should handle the new value.
  • Added: TemplateVersion.from, TemplateVersion.reply_to and
    TemplateVersion.sender_recorded. Template history now records the sender:
    an update that changes only from or reply_to records a version (or folds
    into the open one, like any edit), and templates.restoreVersion brings the
    version's From and Reply-To back. sender_recorded says what a null
    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

Choose a tag to compare

@davepoon davepoon released this 28 Sep 02:45
  • Added: optimistic-concurrency tokens for editing over an API that may also
    be edited in Mailtea Studio or by another agent. Template.revision moves
    on every content-changing write; pass it back as base_revision on
    templates.update and templates.publish to get a 409 stale_write
    (current_revision on the error) instead of silently overwriting someone
    else's edit. automations.update takes base_version the same way for
    graph writes (steps), answering 409 stale_version. Post.updated_at is
    now returned by posts.get/posts.list/posts.update; pass it back as
    base_updated_at on posts.update for a 409 stale_write
    (current_updated_at on the error). Every token is optional: omit it for
    today's unconditional write.

  • Added: Post.from and Post.reply_to, the From and Reply-To set on a post
    (null when the named sender or the publication default decides). The API
    now keeps the from and reply_to you pass to posts.create and
    posts.update instead of dropping them.

  • Changed: name on posts.create / posts.update is only the post's internal
    name and no longer overwrites the subject. posts.update changes only the
    fields you pass, and "" clears name, from or reply_to.

  • Note: a from that is not on one of the publication's verified sending
    domains is now refused with a 422 (the same reason, code and domain as
    emails.send), and reply_to must be a valid address.

  • Docs: automations.activate lists the two cloud-only no_verified_sender
    reasons, CUSTOM_DOMAIN_REQUIRED and the new BUILT_IN_SENDER (a step sends
    from the built-in {slug}.mailtea.email address). 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 example us-east-1 in local
    development or on a self-hosted install). Type-only; no runtime change.

  • Docs: the assets resource 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.create with template_id now HTML-escapes the variables
    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 its fallback_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.update and templates.restoreVersion no 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.publish is called again. The template's from and reply_to
    are part of the published version too, so a new sender or reply-to address
    reaches sends only after the next publish. templates.unpublish is 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_versions on Template and TemplateListItem. 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_current is now described as what it is:
    the entry that matches the working copy (the saved design being edited), not
    necessarily what is sending. is_published is false on every entry of a
    draft, and on a template published before the field existed until it is
    published again.

  • Changed: templates.update returns UpdatedTemplate: the template plus the
    PATCH reply's unpublished and, when the edit is not live yet, message.

  • Changed: RestoredTemplateVersion.unpublished and the PATCH reply's
    unpublished are kept for compatibility and are now always false. Check
    has_unpublished_versions (or the reply's message) instead.

  • Changed (API behavior): a template variable's fallback_value can 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_invalid reply's issues lists just those new problems.
    Before, any error refused the save, even one the live version already had.
    Starting refuses every error as before, except an unknown_step_ref at a
    config.* path or a trigger missing_branch that 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
    with pre_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 their field or, when there is none, their path. 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_existing only if the live version had an error there, not a warning.

  • Changed (API behavior): validate_only on an active automation answers the
    way the save would. A trigger change is a 422 trigger_locked_while_active,
    a change that adds a problem is a 422 active_graph_invalid listing only
    the new problems, and otherwise issues come back with pre_existing marked
    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 422 trigger_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 a missing_branch error at branches.next. A rule or {"var": ...}
    value that reads steps.<key>.* for a step that isn't in the automation is
    an unknown_step_ref error at that config.* path, or a warning when the
    {"var": ...} has a default. A rule or value that reads
    event.properties.* when the automation does not start from an app event is
    the new warning event_field_without_event_trigger.

  • Changed: SendEmailInput.subject is optional when you send a template,
    and from / sender_id may 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: BatchEmailItemInput still requires from and
    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

Choose a tag to compare

@davepoon davepoon released this 15 Sep 11:33
  • Added: test mode. apiKeys.create({ mode: "test" }) mints a test key
    (prefixed mt_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: mode on ListEmailsParams, so emails.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: mode on EmailListItem, RetrievedEmail, CreatedApiKey and
    ApiKeyListItem, plus the EmailMode type — an open union like
    EmailStatus, so a value added server-side still types.
  • Note: mode is never accepted on a send. The key decides.
  • Reserved recipients on test.mailtea.email force an outcome: delivered@,
    bounced@, complained@, delayed@, failed@. The first to recipient
    decides; anything else is delivered.
  • Added: MailteaError.reason — the API's second machine-readable
    discriminator, alongside code. Several routes answer with reason and 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

Choose a tag to compare

@davepoon davepoon released this 10 Sep 21:41
  • Added: receiving_identity_status on domain responses (create, get, update,
    verify). pending, verified, failed, or null when the domain is not
    registered to receive mail yet. Point an MX at Mailtea only once it reads
    verified; it never gates status.
  • Changed: automations.activate documents the no_verified_sender refusal.
    A 422 with that code means a send_email step has no sender it can send
    from; reason (NO_SENDER, DOMAIN_NOT_VERIFIED, WRONG_PURPOSE,
    DKIM_NOT_VERIFIED or INVALID_FROM) says which, and steps[] names every
    blocking step. Previously only automation_invalid was documented.
  • Added: domains.update takes tracking_subdomain: null to 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 with tracking_subdomain_invalid. null is accepted on
    update only; CreateDomainInput.tracking_subdomain stays a string.
  • Changed: the MX row in records now reports what the last verify found,
    instead of reading pending on every request but the verify itself. A domain
    nobody has verified reads not_started.

Released from mailtea-app/mailtea@fed0718.

v0.13.0

Choose a tag to compare

@davepoon davepoon released this 05 Sep 01:00
  • Added: mailtea.domains.claims — create, get, verify and cancel. When
    adding a domain is refused with code domain_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: Domain carries region, tls, tracking_subdomain and
    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_at is set when another publication claimed the host, and a
    released domain cannot send whatever its status says.
  • Added: domains.create takes region, tls and tracking_subdomain;
    domains.update takes the two that are mutable; domains.list takes region
    and status filters.
  • Added: DomainRefusalCode is exported, so a refusal is matched on code
    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: false means links are still served from the platform host.
  • Changed: each row in records now says what it is FOR in record
    (Ownership, DKIM, SPF, MX, Return-Path, Tracking), carries
    ttl: "Auto", and reports its OWN status rather than the domain's — including
    not_started where nothing has checked the record yet. type still holds the
    DNS type and purpose is 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 define first_name and never set
    it. Each entry takes either property_id or key (not both); an empty
    value clears the property, and its fallback_value applies again.

Released from mailtea-app/mailtea@1dff0ba.

v0.12.0

Choose a tag to compare

@davepoon davepoon released this 27 Aug 13:56
  • Added: baseUrl falls back to the MAILTEA_API_BASE_URL environment
    variable before defaulting to https://api.mailtea.app. Point a self-hosted
    or local instance at the SDK without threading an option through your code.
    An explicit baseUrl still 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 fetch is 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 global this; Node and browsers tolerate that, the Workers
    runtime answers TypeError: Illegal invocation. Workers users no longer need
    to pass fetch: globalThis.fetch.bind(globalThis). A fetch you supply is
    still used exactly as given.

Released from mailtea-app/mailtea@a0172ed.

v0.11.0

Choose a tag to compare

@davepoon davepoon released this 25 Aug 12:06
  • 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_date is now string, not
    string | null.
    The server always reports the window it used, so the field
    can no longer be null. Under strictNullChecks a 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_date and
    EmailAnalytics.from_date carry it in their doc comments.

Released from mailtea-app/mailtea@c311c79.