Upgrade from 1.x to 2.0

1.x no longer receives updates, including fixes. Upgrade to 2.0 to keep getting them.

2.0 is a new major version. It moves the Elixir SDK onto the same design as the other Lettermint SDKs: one client for both tokens, the Team API grouped by resource with the same function names in every language, typed errors for every outcome, and types generated from the current API specification. The 1.x strengths stay: clients and builders are immutable, the builder works with the pipe operator, the idempotency key is a per-call option, redirects and retries are off, and decoding never creates atoms.

Highlights

  • One client: Lettermint.new(sending_token: ..., team_token: ...), or Lettermint.new(token). It replaces Lettermint.email(token, opts) and Lettermint.api(token, opts).
  • Each part uses its own token: Lettermint.Emails uses the sending token, the Team API the team token. The SDK never falls back to the other token; a missing token raises Lettermint.ConfigError that names the option.
  • The Team API uses the layout of the other SDKs: Lettermint.Projects.ReportForwarding, Lettermint.Team.Members and Lettermint.Webhooks.Deliveries are their own modules, and every list has an iterate function that returns a Stream.
  • Query parameters are nested maps (%{page: %{size: 30}}) instead of bracketed string keys.
  • Errors are exception structs per kind (Lettermint.NotFoundError, Lettermint.RateLimitError, Lettermint.TimeoutError, …) instead of one %Lettermint.Error{kind: ...}. Network errors keep their reason (1.x reported only :connection_failed).
  • The timeout covers the whole request, including the body.
  • An empty 2xx body where JSON is expected is an error (1.x returned {:ok, nil}). A request body that cannot be encoded as JSON is an error (1.x raised Jason.EncodeError).
  • Webhook verification takes the request headers, requires the X-Lettermint-Delivery header and returns the decoded payload.
  • Tokens are kept inside closures, so they no longer show up in inspect(client, structs: false), :io_lib.format("~p", ...) or crash reports.
  • Types are generated from the current API specification and use its names, under Lettermint.Types (see Type names).
  • Requires Erlang/OTP 25 or later (for :crypto.hash_equals/2). Elixir 1.15+ as before.

Requirements

  • Elixir 1.15 or later.
  • Erlang/OTP 25 or later.

Upgrade with a coding agent

You can let a coding agent (Claude Code, Codex, Cursor, Copilot, …) do the upgrade. Copy this instruction into the agent from your project's root, then review its changes:

Upgrade this project from the `lettermint` Elixir SDK 1.x to 2.0.

1. Change the dependency in mix.exs to `{:lettermint, "~> 2.0"}` and run `mix deps.update lettermint`. 2.0 needs Elixir 1.15+ and Erlang/OTP 25+: check `.tool-versions`, `mix.exs`, CI workflows and Dockerfiles, and report anything older.
2. Read the upgrade guide before changing code: `deps/lettermint/UPGRADE.md`, or https://github.com/lettermint/lettermint-elixir/blob/main/UPGRADE.md. Treat it as the source of truth and don't guess APIs; when unsure, read the source in `deps/lettermint/lib/` or the docs on HexDocs.
3. Find every use of the SDK: `Lettermint.email(`, `Lettermint.api(`, `Lettermint.Email.`, `Lettermint.API.`, `Lettermint.EmailBuilder`, `Lettermint.MessageTag`, `Lettermint.Model.`, `Lettermint.Models.`, `%Lettermint.Error{`, `Lettermint.Webhook.verify(`, the 1.x Team API modules (`Lettermint.Domains`, `Lettermint.Messages`, `Lettermint.Projects`, `Lettermint.Routes`, `Lettermint.Stats`, `Lettermint.Suppressions`, `Lettermint.Team`, `Lettermint.Webhooks`), `query:` and `headers:` request options, `Lettermint.Adapter` implementations, and the 1.x type names from the guide's type-name table.
4. Rewrite each use following the guide's before/after examples:
   - Create one client with `Lettermint.new(sending_token: ...)`, adding `team_token:` only where the Team API is used. Keep the project's existing environment variable names and config.
   - Keep the builder pipelines, but start them with `Lettermint.Emails.compose(client)` instead of `EmailBuilder.new(client)`. Replace `attachments/2` with `attach/2` and `to_map/1` with `build/1`.
   - Keep idempotency keys as the `idempotency_key:` option of `send`/`send_batch`.
   - Team API: pass query parameters as nested maps in the argument before the options (`Lettermint.Domains.list(client, %{page: %{size: 10}})`), not as `query:` with bracketed keys, and use the renamed functions from the guide.
   - Errors: match on the 2.0 exception structs (`%Lettermint.NotFoundError{}`, `%Lettermint.ValidationError{}`, …) or use the guards in `Lettermint.Error`, instead of `%Lettermint.Error{kind: ...}`.
   - Webhooks: `Lettermint.Webhook.new(secret) |> Lettermint.Webhook.verify(raw_body, conn.req_headers)`. Keep passing the raw request body, keep the secret's `whsec_` prefix, and make sure the `X-Lettermint-Signature` and `X-Lettermint-Delivery` headers reach the handler.
   - Rename `Lettermint.Models.*` types to `Lettermint.Types.*` using the guide's type-name table, and `Lettermint.Model.to_map/from_map` to `Lettermint.Types.to_map/decode`.
5. Run `mix format`, `mix compile --warnings-as-errors` and `mix test`, and fix every error. Don't send real email or call the live API while testing.
6. Finish with a summary: the files you changed, anything you could not migrate with certainty, and behaviour changes I should review.

Never print, log or commit API tokens or webhook secrets.

Create the client

Lettermint.email(token, opts) and Lettermint.api(token, opts) are removed. One client holds both tokens.

# 1.x
email = Lettermint.email(System.fetch_env!("LETTERMINT_PROJECT_TOKEN"), timeout: 10_000)
api = Lettermint.api(System.fetch_env!("LETTERMINT_TEAM_TOKEN"))

# 2.0
lettermint =
  Lettermint.new(
    sending_token: System.get_env("LETTERMINT_PROJECT_TOKEN"), # for Lettermint.Emails
    team_token: System.get_env("LETTERMINT_TEAM_TOKEN"),       # for the Team API
    timeout: 10_000
  )

Pass one token or both. With only one token, calling a function that needs the other raises Lettermint.ConfigError (for example domains.list needs :team_token) before any request. 1.x raised ArgumentError ("Wrong client for this API operation").

You can also pass a token string. The SDK chooses the token type by its format:

Lettermint.new("lm_team_...")                    # team token
Lettermint.new("lm_...")                         # project sending token
Lettermint.new(token, timeout: 10_000)           # with options

Any other format (SSO tokens, an empty string) raises Lettermint.ConfigError. Use sending_token: or team_token: for those.

The :base_url, :timeout and :adapter options work as before, with these changes:

Send an email

# 1.x
client = Lettermint.email(token)

client
|> Lettermint.EmailBuilder.new()
|> Lettermint.EmailBuilder.from("Acme <hello@acme.com>")
|> Lettermint.EmailBuilder.to(["jane@example.com"])
|> Lettermint.EmailBuilder.subject("Welcome")
|> Lettermint.EmailBuilder.html("<p>Hi Jane</p>")
|> Lettermint.EmailBuilder.send(idempotency_key: "welcome-jane")

Lettermint.Email.send(client, %{from: "Acme <hello@acme.com>", to: ["jane@example.com"], subject: "Welcome"})

# 2.0
alias Lettermint.EmailBuilder, as: Email

lettermint
|> Lettermint.Emails.compose()
|> Email.from("Acme <hello@acme.com>")
|> Email.to("jane@example.com")
|> Email.subject("Welcome")
|> Email.html("<p>Hi Jane</p>")
|> Email.send(idempotency_key: "welcome-jane")

Lettermint.Emails.send(lettermint, %{from: "Acme <hello@acme.com>", to: ["jane@example.com"], subject: "Welcome"})

Changed builder functions

1.x2.0
EmailBuilder.new(client)Lettermint.Emails.compose(client) or compose(client, message); raises Lettermint.ConfigError without a sending token
attachments(builder, [%{filename:, content:, content_type:}])attach(builder, filename: ..., content: ..., content_type: ..., content_id: ...), once per attachment; content may also be {:bytes, binary}
to_map(builder)build(builder); returns string keys and base64-encodes {:bytes, binary} content
to/2, cc/2, bcc/2, reply_to/2 took any valueTake one address or a list of addresses
html(builder, nil) and the other setters stored nil (sent as JSON null)html/2, text/2, tag/2 and scheduled_at/2 with nil remove the field
scheduled_at(builder, string)scheduled_at(builder, string | DateTime.t())
tags/2 and tag/2 raised ArgumentErrorRaise Lettermint.ClientValidationError (with field); the builder you passed stays valid
—sandbox_result/2
send(builder, opts)send(builder, opts), unchanged: idempotency_key: and now timeout:

Unchanged: from/2, subject/2, text/2, route/2, headers/2, metadata/2, settings/2, tag/2, tags/2. Every function still returns a new builder.

inspect(builder) now shows the message (#Lettermint.EmailBuilder<%{"from" => ...}>) and never the client's tokens. 1.x showed nothing.

Plain messages

Lettermint.Email.send/3 is now Lettermint.Emails.send/3. Messages are still maps in the API's field names with atom or string keys, or a struct (%Lettermint.Types.SendMailRequest{}, was %Lettermint.Models.SendMailRequest{}). Changes:

  • Invalid tags or attachments return {:error, %Lettermint.ClientValidationError{}} before any request. 1.x sent them to the API.
  • A body that cannot be encoded as JSON (invalid UTF-8, a PID, a tuple) returns {:error, %Lettermint.ClientValidationError{field: "body"}}. 1.x raised.
  • Attachment content is base64 text as before, or {:bytes, binary}, which the SDK encodes.
  • Lettermint.MessageTag.new!/2 is removed. Pass %{name: ..., value: ...} maps or %Lettermint.Types.MessageTagInput{} structs; the SDK validates them on send.

Batch sending and ping

# 1.x
Lettermint.Email.send_batch(client, [message1, message2], idempotency_key: "batch-1")
Lettermint.Email.ping(client)
Lettermint.API.ping(api)

# 2.0
Lettermint.Emails.send_batch(lettermint, [message1, message2], idempotency_key: "batch-1")
Lettermint.Emails.send_batch(lettermint, [builder1, builder2]) # builders work too
Lettermint.Emails.ping(lettermint) # sending token
Lettermint.ping(lettermint)        # team token if configured, otherwise the sending token

send_batch/3 returns {:ok, [%Lettermint.Types.SendMailResponse{}]} (was [%Lettermint.Models.SendBatchEmailResponseItem{}]).

Team API

The modules keep their names, but take the one client, and the nested resources have their own modules. Query parameters move from the query: option to an argument before the options, as nested maps. Every list also has an iterate function that returns a Stream.

# 1.x
api = Lettermint.api(token)
{:ok, page} = Lettermint.Domains.list(api, query: %{"page[size]" => 10, "filter[status]" => "verified"})

# 2.0
{:ok, page} = Lettermint.Domains.list(lettermint, %{page: %{size: 10}, filter: %{status: "verified"}})

lettermint
|> Lettermint.Domains.iterate(%{filter: %{status: "verified"}})
|> Enum.each(&IO.puts(&1.domain))
1.x (api = Lettermint.api(token))2.0 (lettermint = Lettermint.new(team_token: token))
Lettermint.API.ping(api)Lettermint.ping(lettermint)
Lettermint.API.blocked_file_types(api)Lettermint.blocked_file_types(lettermint)
Lettermint.API.analytics(api, payload)Lettermint.analytics(lettermint, query)
Lettermint.Domains.list(api, query: params)Lettermint.Domains.list(lettermint, query), Lettermint.Domains.iterate(lettermint, query)
Lettermint.Domains.create(api, payload)Lettermint.Domains.create(lettermint, payload)
Lettermint.Domains.retrieve(api, id)Lettermint.Domains.retrieve(lettermint, id, query \\ %{}) (%{include: ["dnsRecords"]})
Lettermint.Domains.delete(api, id)Lettermint.Domains.delete(lettermint, id)
Lettermint.Domains.verify_dns_records(api, id)Lettermint.Domains.verify_dns_records(lettermint, id)
Lettermint.Domains.verify_dns_record(api, id, record_id)Lettermint.Domains.verify_dns_record(lettermint, id, record_id)
Lettermint.Domains.update_projects(api, id, payload)Lettermint.Domains.update_projects(lettermint, id, payload)
Lettermint.Messages.list(api, query: params)Lettermint.Messages.list(lettermint, query), Lettermint.Messages.iterate(lettermint, query)
Lettermint.Messages.retrieve(api, id)Lettermint.Messages.retrieve(lettermint, id)
Lettermint.Messages.events(api, id, query: params)Lettermint.Messages.events(lettermint, id, query), Lettermint.Messages.iterate_events(lettermint, id, query)
Lettermint.Messages.source(api, id) / html / textunchanged names: Lettermint.Messages.source(lettermint, id) / html / text
Lettermint.Messages.reschedule(client, id, payload)Lettermint.Messages.reschedule(lettermint, id, payload)
Lettermint.Messages.cancel(client, id)Lettermint.Messages.cancel(lettermint, id)
Lettermint.Messages.process(api, id, opts)Lettermint.Messages.process(lettermint, id, idempotency_key: ...)
Lettermint.Projects.list(api, query: params)Lettermint.Projects.list(lettermint, query), Lettermint.Projects.iterate(lettermint, query)
Lettermint.Projects.create(api, payload)Lettermint.Projects.create(lettermint, payload)
Lettermint.Projects.retrieve(api, id)Lettermint.Projects.retrieve(lettermint, id, query \\ %{})
Lettermint.Projects.update(api, id, payload)Lettermint.Projects.update(lettermint, id, payload)
Lettermint.Projects.delete(api, id)Lettermint.Projects.delete(lettermint, id)
Lettermint.Projects.rotate_token(api, id)Lettermint.Projects.rotate_token(lettermint, id) (deprecated by the API)
Lettermint.Projects.retrieve_report_forwarding(api, id)Lettermint.Projects.ReportForwarding.retrieve(lettermint, id)
Lettermint.Projects.update_report_forwarding(api, id, payload)Lettermint.Projects.ReportForwarding.update(lettermint, id, payload)
Lettermint.Projects.delete_report_forwarding(api, id)Lettermint.Projects.ReportForwarding.delete(lettermint, id); returns :ok (1.x: {:ok, nil})
Lettermint.Projects.verify_report_forwarding(api, id, payload)Lettermint.Projects.ReportForwarding.verify(lettermint, id, payload)
Lettermint.Projects.resend_report_forwarding_code(api, id)Lettermint.Projects.ReportForwarding.resend_code(lettermint, id)
Lettermint.Routes.list(api, project_id, query: params)Lettermint.Routes.list(lettermint, project_id, query), Lettermint.Routes.iterate(lettermint, project_id, query)
Lettermint.Routes.create(api, project_id, payload)Lettermint.Routes.create(lettermint, project_id, payload)
Lettermint.Routes.retrieve(api, id)Lettermint.Routes.retrieve(lettermint, id, query \\ %{})
Lettermint.Routes.update(api, id, payload)Lettermint.Routes.update(lettermint, id, payload)
Lettermint.Routes.delete(api, id)Lettermint.Routes.delete(lettermint, id)
Lettermint.Routes.verify_inbound_domain(api, id)Lettermint.Routes.verify_inbound_domain(lettermint, id)
Lettermint.Stats.retrieve(api, query: params)Lettermint.Stats.retrieve(lettermint, %{from: ..., to: ..., project_id: ..., include_machine: ...}) (the query is required)
Lettermint.Suppressions.list(api, query: params)Lettermint.Suppressions.list(lettermint, query), Lettermint.Suppressions.iterate(lettermint, query)
Lettermint.Suppressions.create(api, payload)Lettermint.Suppressions.create(lettermint, payload)
Lettermint.Suppressions.delete(api, id)Lettermint.Suppressions.delete(lettermint, id)
Lettermint.Team.retrieve(api, query: params)Lettermint.Team.retrieve(lettermint, query) (%{include: ["features"]})
Lettermint.Team.update(api, payload)Lettermint.Team.update(lettermint, payload)
Lettermint.Team.usage(api)Lettermint.Team.usage(lettermint)
Lettermint.Team.roles(api)Lettermint.Team.roles(lettermint)
Lettermint.Team.members(api, query: params)Lettermint.Team.Members.list(lettermint, query), Lettermint.Team.Members.iterate(lettermint, query)
Lettermint.Team.retrieve_member(api, user_id)Lettermint.Team.Members.retrieve(lettermint, user_id)
Lettermint.Team.update_member_assignment(api, user_id, payload)Lettermint.Team.Members.update_assignment(lettermint, user_id, payload)
Lettermint.Webhooks.list(api, query: params)Lettermint.Webhooks.list(lettermint, query), Lettermint.Webhooks.iterate(lettermint, query)
Lettermint.Webhooks.create(api, payload)Lettermint.Webhooks.create(lettermint, payload)
Lettermint.Webhooks.retrieve(api, id)Lettermint.Webhooks.retrieve(lettermint, id)
Lettermint.Webhooks.update(api, id, payload)Lettermint.Webhooks.update(lettermint, id, payload)
Lettermint.Webhooks.delete(api, id)Lettermint.Webhooks.delete(lettermint, id)
Lettermint.Webhooks.test(api, id)Lettermint.Webhooks.test(lettermint, id)
Lettermint.Webhooks.regenerate_secret(api, id)Lettermint.Webhooks.regenerate_secret(lettermint, id)
Lettermint.Webhooks.deliveries(api, id, query: params)Lettermint.Webhooks.Deliveries.list(lettermint, id, query), Lettermint.Webhooks.Deliveries.iterate(lettermint, id, query)
Lettermint.Webhooks.show_delivery(api, id, delivery_id)Lettermint.Webhooks.Deliveries.retrieve(lettermint, id, delivery_id)

Lettermint.Messages.reschedule/4 and cancel/3 accept either token: the team token when configured, otherwise the sending token. This lets a sending-only client cancel the scheduled email it sent.

Request options

Every function takes a keyword list of options last.

1.x2.0
query: %{...}Removed: pass the query map as its own argument, before the options. Passing a keyword list where the query goes raises Lettermint.ConfigError.
headers: %{...}Removed.
idempotency_key: keyUnchanged, on Lettermint.Emails.send/3, send_batch/3, Lettermint.EmailBuilder.send/2 and Lettermint.Messages.process/3 only.
—timeout: ms overrides the client's timeout for one call.

Unknown options raise Lettermint.ConfigError (1.x: ArgumentError).

Query parameters

Write bracketed names as nested maps (atom or string keys). Lists of values are joined with commas, lists of maps are indexed, and booleans are sent as 1/0.

1.x (query:)2.0
%{"page[size]" => 30, "page[cursor]" => c}%{page: %{size: 30, cursor: c}}
%{"filter[status]" => "verified"}%{filter: %{status: "verified"}}
%{"sort" => "-created_at,domain"}%{sort: ["-created_at", "domain"]}
%{"filter[tags][0][name]" => "a", "filter[tags][0][value]" => "b"}%{filter: %{tags: [%{name: "a", value: "b"}]}}
%{"filter[enabled]" => "true"}%{filter: %{enabled: true}}
webhooks: %{"cursor" => c}unchanged: %{cursor: c} (these lists use cursor, not page[cursor])

Path parameters

IDs are still URL-encoded. An empty ID, "." or ".." raises Lettermint.ConfigError before the request, as in 1.x (which raised ArgumentError).

Lists and pagination

List responses decode into %Lettermint.Types.CursorPage{data: [...], next_cursor: ..., ...} (1.x had one struct per list, such as %Lettermint.Models.DomainIndexResponse{}, with the same fields). Read page.next_cursor, or use iterate, which follows it for you and stops on a repeated cursor. A stream raises the error of a failed page, because a stream cannot return {:error, error}.

Errors

%Lettermint.Error{kind: ..., status: ..., body: ...} is replaced by one exception struct per kind. Lettermint.Error is now a module with guards (is_lettermint_error/1, is_api_error/1).

Situation1.x2.0
HTTP 400 and other 4xx%Lettermint.Error{kind: :api, status: s, body: body}%Lettermint.APIError{status, code, message, details, body}
HTTP 401kind: :api%Lettermint.AuthenticationError{}
HTTP 403kind: :api%Lettermint.PermissionError{}
HTTP 404kind: :api%Lettermint.NotFoundError{}
HTTP 409kind: :api%Lettermint.ConflictError{}
HTTP 422kind: :api%Lettermint.ValidationError{errors: %{"field" => [...]}}
HTTP 429kind: :api%Lettermint.RateLimitError{retry_after: seconds}
HTTP 5xxkind: :api%Lettermint.ServerError{}
Redirect (3xx)kind: :api with the 3xx status%Lettermint.RedirectError{status} (never followed, as before)
HTML error page, invalid JSONkind: :api with the raw body, or kind: :decode%Lettermint.UnexpectedResponseError{status, body_excerpt}
Empty 2xx body where JSON is expected{:ok, nil}%Lettermint.UnexpectedResponseError{status}
Timeoutkind: :transport%Lettermint.TimeoutError{timeout}; covers the whole request
Network failurekind: :transport (:connection_failed)%Lettermint.ConnectionError{reason}, with the HTTP client's reason
A response that does not match its typekind: :decodeDecoded anyway: unexpected values are kept as decoded from JSON
Invalid tags or attachmentsArgumentError (builder) or sent to the API%Lettermint.ClientValidationError{field}
A body that cannot be encoded as JSONJason.EncodeError raised%Lettermint.ClientValidationError{field: "body"}
Missing or wrong token, bad option or IDArgumentError raisedLettermint.ConfigError raised

code comes from {"error": {"code": ...}} or a string error field. message is the API's message, or "HTTP <status>". Every error is an exception, so you can also raise error.

# 1.x
case Lettermint.Email.send(client, message) do
  {:ok, result} -> result
  {:error, %Lettermint.Error{kind: :api, status: 422, body: body}} -> {:invalid, body}
  {:error, %Lettermint.Error{kind: :api, status: 429}} -> :retry_later
  {:error, %Lettermint.Error{kind: kind}} -> {:failed, kind}
end

# 2.0
case Lettermint.Emails.send(lettermint, message) do
  {:ok, result} -> result
  {:error, %Lettermint.ValidationError{errors: errors}} -> {:invalid, errors}
  {:error, %Lettermint.RateLimitError{retry_after: seconds}} -> {:retry_later, seconds}
  {:error, error} -> {:failed, error}
end

The SDK does not retry requests. Pass an idempotency_key when you retry a send.

Webhooks

# 1.x
:ok = Lettermint.Webhook.verify(raw_body, signature_header, secret, tolerance: 300)

# 2.0
webhook = Lettermint.Webhook.new(secret, tolerance: 300)
{:ok, payload} = Lettermint.Webhook.verify(webhook, raw_body, conn.req_headers)
{:ok, payload} = Lettermint.Webhook.verify_signature(webhook, raw_body, signature_header, timestamp: delivery_header)
  • The secret moves into a verifier struct, Lettermint.Webhook.new/2. An empty secret or a negative tolerance raises Lettermint.ConfigError (1.x returned {:error, :invalid_signature}).
  • verify/4 takes the request headers (a map, a list such as conn.req_headers, or a Plug.Conn) and requires both X-Lettermint-Signature and X-Lettermint-Delivery, which must equal the signed timestamp. 1.x did not read the delivery header.
  • On success it returns {:ok, %Lettermint.WebhookPayload{id, event, timestamp, data, extra}} (1.x: :ok), so you no longer decode the body yourself.
  • Failures return {:error, %Lettermint.WebhookVerificationError{reason: reason}} instead of {:error, :invalid_signature}. The reasons are :signature_header_missing, :signature_header_malformed, :delivery_header_missing, :delivery_timestamp_mismatch, :timestamp_out_of_tolerance, :signature_mismatch, :body_invalid and :payload_invalid.
  • The :now option (Unix seconds) works as before, on verify/4 and verify_signature/4. :tolerance moves to new/2.

Adapters

The Lettermint.Adapter behaviour changed so that the SDK can read response headers (Retry-After, Content-Type):

1.x2.0
@callback request(map()) :: {:ok, status, body} | {:error, term()}@callback request(request :: map(), options :: keyword()) :: {:ok, %{status: status, headers: headers, body: body}} | {:error, reason}
request.headers was a maprequest.headers is a list of {name, value}
Lettermint.HTTP (internal)Lettermint.Adapter.Req; pass Req options as adapter: {Lettermint.Adapter.Req, options}

The SDK runs the adapter in a task and enforces the timeout itself. {:error, reason} becomes Lettermint.ConnectionError with that reason (:timeout becomes Lettermint.TimeoutError).

Type names

The types are generated from the API specification of lettermint#2582 and use its names. They move from Lettermint.Models to Lettermint.Types. Lettermint.Model.to_map/1 is now Lettermint.Types.to_map/1, and Lettermint.Model.from_map/2 is Lettermint.Types.decode/2. Some shapes also changed:

  • Enums are open, as before: unknown values decode as strings, and enum modules keep values/0.
  • SendMailResponse (was SendEmailResponse and SendBatchEmailResponseItem) holds the fields of a pending and of a scheduled send; check status.
  • MessageTag describes tags in responses; MessageTagInput describes tags you send (was SendMailRequestTagsItem).
  • Every list is Lettermint.Types.CursorPage (see Lists and pagination). The per-list modules, such as ListDomainsResponse, are typespecs for CursorPage.t(item).
  • Query parameters have typespec modules, such as Lettermint.Types.ListDomainsQuery.
  • Typespecs now tell required fields (T), optional fields (T | :unset) and nullable fields (T | nil) apart. 1.x typed every field as T | nil | :unset.

These types keep their name and only move from Lettermint.Models to Lettermint.Types:

AttachmentDelivery, BuiltInTeamRole, DeliveryMode, DkimMode, DnsRecordPurpose, DnsRecordStatus, DnsVerificationScope, DomainData, DomainDataProjectsItem, DomainDnsRecordData, DomainListData, DomainStatus, GetReportForwardingResponse, InitialRoutes, MessageAttachmentData, MessageData, MessageEventData, MessageEventType, MessageListData, MessageRecipientData, MessageStatsData, MessageStatus, MessageType, Plan, ProcessInboundMessageResponse, ProjectAccessScope, ProjectCreatedData, ProjectData, ProjectListData, RbacConflictCode, RbacPermission, RecordType, ReportForwardingRequest, ReportForwardingResource, RescheduleMessageRequest, ResendReportForwardingCodeResponse, RouteData, RouteDataSettings, RouteListData, RouteStatisticData, RouteType, SandboxResult, SendMailRequest, SendMailRequestSettings, SpamSymbol, StatsDailyData, StatsData, StatsInboundData, StatsTotalsData, StatsTypeData, StoreDomainData, StoreProjectData, StoreRouteData, StoreSuppressionData, StoreWebhookData, SuppressedRecipientData, SuppressionAppliesTo, SuppressionReason, SuppressionScope, SuppressionSourceMessageData, SuppressionStoreResponse, SuppressionType, TeamAddonData, TeamData, TeamMemberData, TeamMemberDataRole, TeamMemberProjectAccessData, TeamMemberProjectAccessDataProjectsItem, TeamRoleData, TeamType, TeamUsageDetailData, TeamUsagePeriodData, TlsPolicy, UpdateDomainProjectsData, UpdateProjectData, UpdateReportForwardingResponse, UpdateRouteData, UpdateRouteInboundSettingsData, UpdateRouteSettingsData, UpdateTeamData, UpdateTeamMemberAssignmentData, UpdateTeamMemberAssignmentDataProjectAccess, UpdateWebhookData, VerifyReportForwardingRequest, VerifyReportForwardingResponse, WebhookBasicAuthData, WebhookData, WebhookDeliveryData, WebhookDeliveryListData, WebhookDeliveryModeFilter, WebhookDeliveryStatus, WebhookEvent, WebhookListData, WebhookScope, WebhookSecretData

These types are renamed:

1.x (Lettermint.Models.*)2.0 (Lettermint.Types.*)
CancelScheduledMessageResponseScheduledMessage
DomainDestroyResponseMessageResponse
DomainIndexResponseListDomainsResponse
DomainUpdateProjectsResponseDomainMutationResponse
DomainVerifyDnsRecordsResponseDnsVerificationSuccessResponse
DomainVerifyDnsRecordsResponseRecommendedFailedRecordsItemDnsVerificationFailedRecord
DomainVerifySpecificDnsRecordResponseMessageResponse
MessageDataTagsItemMessageTag
MessageEventDataTagsItemMessageTag
MessageEventsResponseListMessageEventsResponse
MessageIndexResponseListMessagesResponse
MessageListDataTagsItemMessageTag
ProcessInboundMessageResponseDataProcessInboundMessageResult
ProjectDestroyResponseMessageResponse
ProjectIndexResponseListProjectsResponse
ProjectRotateTokenResponseRotateProjectTokenResponse
ProjectStoreResponseProjectCreatedData
ProjectUpdateResponseProjectMutationResponse
RescheduleMessageResponseScheduledMessage
RouteDestroyResponseMessageResponse
RouteIndexResponseListRoutesResponse
RouteStoreResponseRouteMutationResponse
RouteUpdateResponseRouteMutationResponse
RouteVerifyInboundDomainResponseInboundDomainVerificationResponse
RouteVerifyInboundDomainResponseDataInboundDomainVerification
SendBatchEmailResponseItemSendMailResponse
SendBatchMailRequestItemSendMailRequest
SendBatchMailRequestItemAttachmentsItemMessageAttachmentInput
SendBatchMailRequestItemSettingsSendMailRequestSettings
SendBatchMailRequestItemTagsItemMessageTagInput
SendEmailResponseSendMailResponse
SendMailRequestAttachmentsItemMessageAttachmentInput
SendMailRequestTagsItemMessageTagInput
SuppressionDestroyResponseDeleteSuppressionResponse
SuppressionIndexResponseListSuppressionsResponse
SuppressionStoreResponseDataSuppressionStoreResult
TeamMembersResponseListTeamMembersResponse
TeamRolesResponseTeamRoleListResponse
TeamUpdateResponseTeamMutationResponse
V1AnalyticsRequestAnalyticsQuery
V1AnalyticsRequestCompareAnalyticsComparison
V1AnalyticsRequestFiltersItemAnalyticsFilter
V1AnalyticsRequestFiltersItemOperatorAnalyticsFilterOperator
V1AnalyticsRequestIncludeItemAnalyticsSection
V1AnalyticsRequestIntervalAnalyticsInterval
V1AnalyticsRequestMetricsItemAnalyticsMetric
V1AnalyticsRequestSortAnalyticsSort
V1AnalyticsRequestSortDirectionAnalyticsSortDirection
V1AnalyticsRequestSortMetricAnalyticsMetric
V1AnalyticsResponseAnalyticsResponse
V1AnalyticsResponseDataAnalyticsResults
V1AnalyticsResponseDataBreakdownItemAnalyticsBreakdownRow
V1AnalyticsResponseDataBreakdownItemChangeValueAnalyticsMetricChange
V1AnalyticsResponseDataBreakdownItemMetricsAnalyticsMetricValues
V1AnalyticsResponseDataBreakdownItemPreviousAnalyticsComparisonValues
V1AnalyticsResponseDataBreakdownItemPreviousMetricsAnalyticsMetricValues
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesAnalyticsRateBases
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesAnalyticsRateBases
V1AnalyticsResponseDataBreakdownItemRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemAnalyticsTimeSeriesPoint
V1AnalyticsResponseDataBreakdownItemTrendItemChangeValueAnalyticsMetricChange
V1AnalyticsResponseDataBreakdownItemTrendItemMetricsAnalyticsMetricValues
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousAnalyticsComparisonValues
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousMetricsAnalyticsMetricValues
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesAnalyticsRateBases
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesAnalyticsRateBases
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryAnalyticsSummary
V1AnalyticsResponseDataSummaryChangeValueAnalyticsMetricChange
V1AnalyticsResponseDataSummaryMetricsAnalyticsMetricValues
V1AnalyticsResponseDataSummaryPreviousAnalyticsComparisonValues
V1AnalyticsResponseDataSummaryPreviousMetricsAnalyticsMetricValues
V1AnalyticsResponseDataSummaryPreviousRateBasesAnalyticsRateBases
V1AnalyticsResponseDataSummaryPreviousRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryPreviousRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryPreviousRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryPreviousRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryPreviousRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryPreviousRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryPreviousRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesAnalyticsRateBases
V1AnalyticsResponseDataSummaryRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataSummaryRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemAnalyticsTimeSeriesPoint
V1AnalyticsResponseDataTimeSeriesItemChangeValueAnalyticsMetricChange
V1AnalyticsResponseDataTimeSeriesItemMetricsAnalyticsMetricValues
V1AnalyticsResponseDataTimeSeriesItemPreviousAnalyticsComparisonValues
V1AnalyticsResponseDataTimeSeriesItemPreviousMetricsAnalyticsMetricValues
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesAnalyticsRateBases
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesAnalyticsRateBases
V1AnalyticsResponseDataTimeSeriesItemRateBasesBounceRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesComplaintRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesDeferralRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesEffectiveDeliveryRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesHumanClickRateAnalyticsRateBase
V1AnalyticsResponseDataTimeSeriesItemRateBasesHumanOpenRateAnalyticsRateBase
V1AnalyticsResponseMetaAnalyticsMeta
V1AnalyticsResponseMetaAlignmentAnalyticsMetaAlignment
V1AnalyticsResponseMetaCollectionCompletenessAnalyticsMetaCollectionCompleteness
V1AnalyticsResponseMetaComparisonAnalyticsMetaComparison
V1AnalyticsResponseMetaIntervalAnalyticsInterval
V1AnalyticsResponseMetaTimeBasisAnalyticsMetaTimeBasis
V1AnalyticsResponsePaginationAnalyticsPagination
V1BlockedFileTypesResponseBlockedFileTypes
WebhookDeliveriesResponseListWebhookDeliveriesResponse
WebhookDestroyResponseMessageResponse
WebhookIndexResponseListWebhooksResponse
WebhookRegenerateSecretResponseWebhookSecretResponse
WebhookStoreResponseWebhookSecretResponse
WebhookTestResponseTestWebhookResponse
WebhookUpdateResponseWebhookMutationResponse

Removed types

lettermint#2582 removed these schemas from the API specification. 1.x exported all five:

1.x2.0
Lettermint.Models.AnalyticsResponseDataRemoved. Use Lettermint.Types.AnalyticsResponse (data: AnalyticsResults).
Lettermint.Models.StatsRequestDataRemoved. Use Lettermint.Types.GetStatsQuery, the query of Lettermint.Stats.retrieve/3.
Lettermint.Models.MessageIndexResponseMetaRemoved. Message lists are flat CursorPage structs; read page.next_cursor.
Lettermint.Models.MessageEventsResponseMetaRemoved. Event lists are flat CursorPage structs.
Lettermint.Models.SuppressionStoreResponseMessage1Removed. SuppressionStoreResponse.message is a string.

Removed modules and functions

1.x2.0
Lettermint.email(token, opts)Lettermint.new([sending_token: token] ++ opts)
Lettermint.api(token, opts)Lettermint.new([team_token: token] ++ opts)
%Lettermint.Client{surface: ..., token: ...}%Lettermint.Client{} holds both tokens, in closures; build it with Lettermint.new/1,2
Lettermint.EmailLettermint.Emails (send/3, send_batch/3, ping/2, and the new compose/2)
Lettermint.APILettermint.ping/2, Lettermint.analytics/3, Lettermint.blocked_file_types/2
Lettermint.EmailBuilder.new(client)Lettermint.Emails.compose/2
Lettermint.EmailBuilder.attachments(builder, list)Lettermint.EmailBuilder.attach/2
Lettermint.EmailBuilder.to_map(builder)Lettermint.EmailBuilder.build/1
Lettermint.MessageTag (new!/2)%{name: ..., value: ...} maps or %Lettermint.Types.MessageTagInput{}, validated on send
Lettermint.Model.to_map/1, from_map/2Lettermint.Types.to_map/1, Lettermint.Types.decode/2
Lettermint.Models.*Lettermint.Types.* (see Type names)
%Lettermint.Error{kind: :api | :transport | :decode}One exception per kind (see Errors); Lettermint.Error has guards
Lettermint.HTTPLettermint.Adapter.Req
Lettermint.Webhook.verify(body, signature, secret, opts)Lettermint.Webhook.verify_signature(Lettermint.Webhook.new(secret), body, signature, opts), or verify/4 with the headers
Lettermint.Projects.retrieve_report_forwarding(client, id) and the other *_report_forwarding functionsLettermint.Projects.ReportForwarding
Lettermint.Team.members(client, opts), retrieve_member, update_member_assignmentLettermint.Team.Members
Lettermint.Webhooks.deliveries(client, id, opts), show_deliveryLettermint.Webhooks.Deliveries
query: and headers: request optionsA query argument; custom headers are removed
specs/operations.jsonLettermint.Operations (generated)