1.x no longer receives updates, including fixes. Upgrade to 2.0 to keep getting them.
2.0 is a new major version. It brings the .NET SDK in line with the other Lettermint SDKs: one client for both tokens, stateless sending with an immutable builder, typed errors for every outcome, open enums that never fail on a value the API adds later, typed query parameters with pagination helpers, and webhook verification that requires both signature headers.
- One client:
new LettermintClient(new LettermintOptions { SendingToken, TeamToken }), ornew LettermintClient(token). It replacesLettermintClient.Email()andLettermintClient.Api(). - Sending is stateless:
Emails.SendAsync(message, options),Emails.SendBatchAsync(messages, options)and an immutableEmails.Compose()builder. The idempotency key is a per-call option. - Each part uses its own token:
Emailsuses the sending token, the Team API uses the team token. The SDK never falls back to the other token. - Typed exceptions for every outcome, including empty or HTML responses, redirects, timeouts and network failures.
- Redirects are never followed, so tokens are never sent to another host.
- Tokens never appear in
ToString(), the debugger or JSON output of SDK objects. - Enums are open structs: a status the API adds later decodes instead of failing the call.
- Typed query records (
new ListDomainsQuery { PageSize = 30 }) andIterateAsync()helpers that follownext_cursor. - Types are generated from the current API specification and use its names (see Type names).
- Webhook verification is an instance class, takes the request headers and requires
X-Lettermint-Delivery.
.NET 8 or .NET 10, as before. The package has no dependencies.
You can let a coding agent (Claude Code, Codex, Cursor, Copilot, …) do the upgrade. Copy this instruction into the agent from your solution's root, then review its changes:
Upgrade this solution from the Lettermint .NET SDK 1.x (NuGet package `Lettermint`) to 2.0.
1. Update the package in every project that references it: `dotnet add <project> package Lettermint --version 2.0.0` (or edit the PackageReference / Directory.Packages.props). 2.0 targets .NET 8 and .NET 10; report any project that targets an older framework.
2. Read the upgrade guide before changing code: `UPGRADE.md` in the package folder (`~/.nuget/packages/lettermint/2.0.0/UPGRADE.md`, or `%UserProfile%\.nuget\packages\lettermint\2.0.0\UPGRADE.md` on Windows), or https://github.com/lettermint/lettermint-dotnet/blob/main/UPGRADE.md. Treat it as the source of truth and don't guess APIs; when unsure, read the XML docs in `lib/net8.0/Lettermint.xml` in the same folder.
3. Find every use of the SDK: `using Lettermint`, `LettermintClient.Email(`, `LettermintClient.Api(`, `EmailClient`, `ApiClient`, `*Endpoint`, `ClientOptions`, `RequestOptions`, `.IdempotencyKey(`, `.Attach(`, `MessageTag`, `OptionalNullable`, `Webhook.Verify(`, `LettermintApiException`, `ResponseBody`, `JsonElement` payload overloads, `switch` statements over Lettermint enums, 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 `LettermintClient` with `new LettermintOptions { SendingToken = ... }`, adding `TeamToken` only where the Team API is used. Register it once (for example as a singleton) instead of creating clients per request. Keep the project's existing configuration keys and environment variable names.
- Replace builder chains with `lettermint.Emails.SendAsync(new SendMailRequest { ... })`, or with `lettermint.Emails.Compose()` per email. Builders are immutable: assign the result of every setter. Never keep a builder that collects state across emails.
- Move idempotency keys into `new IdempotentRequestOptions { IdempotencyKey = ... }`. Attachments take raw bytes or `EmailAttachment.FromBase64(...)`.
- Team API: use the same client, typed query records instead of `RequestOptions.Query` dictionaries, and the renamed methods from the guide.
- Exceptions: switch to the 2.0 classes (`LettermintApiException`, `ValidationException`, `RateLimitException`, `LettermintTimeoutException`, …).
- Enums are structs now: replace `case MessageStatus.Delivered:` with `==` comparisons or `switch` on `ToString()`, and keep a default branch for values the API adds later.
- Webhooks: `new Webhook(secret).Verify(rawBody, request.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 types using the guide's type-name table.
5. Run `dotnet build` with warnings as errors where the project uses them, and `dotnet 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.
LettermintClient.Email(), LettermintClient.Api(), EmailClient, ApiClient and ClientOptions are removed. LettermintClient is now the client itself.
// 1.x
using var email = LettermintClient.Email(sendingToken, new ClientOptions { Timeout = TimeSpan.FromSeconds(10) });
using var api = LettermintClient.Api(teamToken);
// 2.0
using var lettermint = new LettermintClient(new LettermintOptions
{
SendingToken = Environment.GetEnvironmentVariable("LETTERMINT_PROJECT_TOKEN"), // lettermint.Emails
TeamToken = Environment.GetEnvironmentVariable("LETTERMINT_TEAM_TOKEN"), // the Team API
Timeout = TimeSpan.FromSeconds(10),
});Pass one token or both. With only one token, calling a part that needs the other throws LettermintConfigException (for example Domains.ListAsync needs TeamToken; …) before any request.
You can also pass a token string. The SDK chooses the token type by its format:
var lettermint = new LettermintClient("lm_team_..."); // team token
var lettermint = new LettermintClient("lm_..."); // project sending token
var lettermint = new LettermintClient(token, new LettermintOptions { Timeout = TimeSpan.FromSeconds(10) });Any other format (SSO tokens, OAuth tokens, an empty string) throws LettermintConfigException. Use SendingToken or TeamToken for those. Setting a token in the options as well as passing one as the first argument also throws.
1.x ClientOptions |
2.0 LettermintOptions |
|---|---|
BaseUrl (Uri) |
BaseUrl (Uri), unchanged |
Timeout |
Timeout, unchanged; it now also covers reading the body |
HttpClient |
HttpClient, unchanged: still owned by you, its handler must not follow redirects, and it must not have default auth headers |
token argument of Email() / Api() |
SendingToken / TeamToken |
The SDK-owned HttpClient is disposed by lettermint.Dispose(), as in 1.x.
In 1.x, From() and Compose() returned a mutable builder that kept its values after a send, including the idempotency key. In 2.0, Emails.Compose() returns an immutable builder: each setter returns a new builder and leaves the old one unchanged. Chaining works as before. If you built an email over several statements, assign the result of each setter.
// 1.x
var result = await email.From("Acme <hello@acme.com>")
.To("jane@example.com")
.Subject("Welcome")
.Html("<p>Hi Jane</p>")
.IdempotencyKey("welcome-jane")
.SendAsync();
// 2.0: builder
var result = await lettermint.Emails.Compose()
.From("Acme <hello@acme.com>")
.To("jane@example.com")
.Subject("Welcome")
.Html("<p>Hi Jane</p>")
.SendAsync(new IdempotentRequestOptions { IdempotencyKey = "welcome-jane" });
// 2.0: a message record
var result = await lettermint.Emails.SendAsync(
new SendMailRequest { From = "Acme <hello@acme.com>", To = ["jane@example.com"], Subject = "Welcome", Html = "<p>Hi Jane</p>" },
new IdempotentRequestOptions { IdempotencyKey = "welcome-jane" });// 1.x: statements mutated the builder
var builder = email.Compose();
builder.From("hello@acme.com").To("jane@example.com");
if (copy) builder.Cc("team@acme.com");
await builder.Subject("Hi").SendAsync();
// 2.0: keep the returned builder
var draft = lettermint.Emails.Compose().From("hello@acme.com").To("jane@example.com");
if (copy) draft = draft.Cc("team@acme.com");
await draft.Subject("Hi").SendAsync();A base builder can now be shared and reused safely, also across concurrent requests.
| 1.x | 2.0 |
|---|---|
email.From(x), email.Compose() |
lettermint.Emails.Compose().From(x), lettermint.Emails.Compose() |
| Setters change the builder and return it | Setters return a new builder |
.IdempotencyKey(key).SendAsync(ct) |
.SendAsync(new IdempotentRequestOptions { IdempotencyKey = key }, ct) |
.Attach(filename, base64, contentId?, contentType?) |
.Attach(filename, bytes, contentType?, contentId?) or .Attach(EmailAttachment.FromBase64(filename, base64) with { ContentType = …, ContentId = … }) |
.Tags(params MessageTag[]), .Tags(params SendMailRequestTagsItem[]) |
.Tags(params MessageTagInput[]) or .Tags(IEnumerable<MessageTagInput>) |
.Tags() / .Tag() threw ArgumentException |
Throw LettermintValidationException (with Field); the builder stays unchanged |
.Html(null), .Text(null), .Route(null) |
Unchanged: null removes the field; so do Tag(null) and ScheduledAt(null) |
.ScheduledAt(string) |
.ScheduledAt(string?) or .ScheduledAt(DateTimeOffset) |
.Settings(settings), .SandboxResult(result) |
Unchanged; both accept null to remove the field |
.SendAsync(ct) returns SendEmailResponse |
.SendAsync(options?, ct) returns SendMailResponse; the builder can be sent again |
| — | .Build() returns the message as a SendMailRequest |
Unchanged setters: To, Cc, Bcc, ReplyTo (each replaces its list), Subject, Headers, Metadata, Tag.
MessageTag (the hand-written tag record that validated in its constructor) is removed. Use new MessageTagInput { Name = …, Value = … }; tags are validated when you set them on a builder and before every send. MessageValidation.ValidateTags(tags, hasLegacyTag) runs the check on its own. MessageTag is now the generated type of tags in responses.
SendMailRequest and the other request types are records now (init properties, required for required fields). Optional fields that the API accepts as null are Optional<T?> instead of OptionalNullable<T>:
// 1.x
var settings = new UpdateRouteSettingsData { TrackOpens = true };
var clear = new UpdateWebhookData { BasicAuth = OptionalNullable<WebhookBasicAuthData>.Null };
// 2.0
var settings = new UpdateRouteSettingsData { TrackOpens = true };
var clear = new UpdateWebhookData { BasicAuth = null }; // sends "basic_auth": null
var keep = new UpdateWebhookData(); // leaves basic_auth outRead an Optional<T> with .IsSet and .Value. SendMailRequest.Html, Text and Tag are Optional<string?>: assigning a string works as before, and reading needs .Value.
The JsonElement payload overloads (SendAsync(JsonElement), CreateAsync(JsonElement), …) are removed. To send a field the SDK does not know yet, put it in the record's AdditionalProperties.
// 1.x
await email.SendBatchAsync(new SendMailRequest[] { message1, message2 }, new RequestOptions { IdempotencyKey = "batch-1" });
await email.SendBatchAsync(new List<SendBatchMailRequestItem> { item1 });
await email.PingAsync();
await api.PingAsync();
await email.RescheduleAsync(messageId, new RescheduleMessageRequest { ScheduledAt = "tomorrow at 10am" });
await email.CancelAsync(messageId);
// 2.0
await lettermint.Emails.SendBatchAsync([message1, message2], new IdempotentRequestOptions { IdempotencyKey = "batch-1" });
await lettermint.Emails.SendBatchAsync([builder1, builder2]); // builders work too
await lettermint.Emails.PingAsync(); // sending token
await lettermint.PingAsync(); // team token if configured, otherwise the sending token
await lettermint.Messages.RescheduleAsync(messageId, new RescheduleMessageRequest { ScheduledAt = "tomorrow at 10am" });
await lettermint.Messages.CancelAsync(messageId);SendBatchAsync returns IReadOnlyList<SendMailResponse> (1.x: List<SendBatchEmailResponseItem>). Messages.RescheduleAsync and Messages.CancelAsync accept either token: the team token when configured, otherwise the sending token, so a sending-only client can still reschedule and cancel what it sent.
The endpoint classes move from LettermintClient.Api(token).X to lettermint.X. Query parameters are typed records instead of RequestOptions.Query dictionaries. Every list also has an IterateAsync() method that follows next_cursor.
// 1.x
using var api = LettermintClient.Api(teamToken);
var page = await api.Domains.ListAsync(new RequestOptions
{
Query = new Dictionary<string, string> { ["page[size]"] = "10", ["filter[status]"] = "verified" },
});
// 2.0
var page = await lettermint.Domains.ListAsync(new ListDomainsQuery { PageSize = 10, FilterStatus = DomainStatus.Verified });
await foreach (var domain in lettermint.Domains.IterateAsync(new ListDomainsQuery { FilterStatus = DomainStatus.Verified }))
{
Console.WriteLine(domain.Domain);
}1.x (api = LettermintClient.Api(token)) |
2.0 (lettermint = new LettermintClient(options)) |
|---|---|
api.PingAsync() |
lettermint.PingAsync() |
api.BlockedFileTypesAsync() |
lettermint.BlockedFileTypesAsync() |
api.AnalyticsAsync(V1AnalyticsRequest) |
lettermint.AnalyticsAsync(AnalyticsQuery) |
api.Domains.ListAsync(options) |
lettermint.Domains.ListAsync(query?), lettermint.Domains.IterateAsync(query?) |
api.Domains.CreateAsync(payload) |
lettermint.Domains.CreateAsync(payload) |
api.Domains.RetrieveAsync(id) |
lettermint.Domains.RetrieveAsync(id, query?) (Include = [GetDomainQueryIncludeItem.DnsRecords]) |
api.Domains.DeleteAsync(id) |
lettermint.Domains.DeleteAsync(id) |
api.Domains.VerifyDnsRecordsAsync(id) |
lettermint.Domains.VerifyDnsRecordsAsync(id) |
api.Domains.VerifyDnsRecordAsync(id, recordId) |
lettermint.Domains.VerifyDnsRecordAsync(id, recordId) |
api.Domains.UpdateProjectsAsync(id, payload) |
lettermint.Domains.UpdateProjectsAsync(id, payload) |
api.Messages.ListAsync(options) |
lettermint.Messages.ListAsync(query?), lettermint.Messages.IterateAsync(query?) |
api.Messages.RetrieveAsync(id) |
lettermint.Messages.RetrieveAsync(id) |
api.Messages.EventsAsync(id) |
lettermint.Messages.EventsAsync(id, query?), lettermint.Messages.IterateEventsAsync(id, query?) |
api.Messages.SourceAsync(id) / HtmlAsync(id) / TextAsync(id) |
unchanged, on lettermint.Messages |
api.Messages.RescheduleAsync(id, payload) |
lettermint.Messages.RescheduleAsync(id, payload) |
api.Messages.CancelAsync(id) |
lettermint.Messages.CancelAsync(id) |
api.Messages.ProcessAsync(id, new RequestOptions { IdempotencyKey }) |
lettermint.Messages.ProcessAsync(id, new IdempotentRequestOptions { IdempotencyKey }) |
api.Projects.ListAsync(options) |
lettermint.Projects.ListAsync(query?), lettermint.Projects.IterateAsync(query?) |
api.Projects.CreateAsync(payload) |
lettermint.Projects.CreateAsync(payload) |
api.Projects.RetrieveAsync(id) |
lettermint.Projects.RetrieveAsync(id, query?) |
api.Projects.UpdateAsync(id, payload) |
lettermint.Projects.UpdateAsync(id, payload) |
api.Projects.DeleteAsync(id) |
lettermint.Projects.DeleteAsync(id) |
api.Projects.RotateTokenAsync(id) |
lettermint.Projects.RotateTokenAsync(id) (marked [Obsolete]: the API calls it legacy) |
api.Projects.RetrieveReportForwardingAsync(id) |
lettermint.Projects.ReportForwarding.RetrieveAsync(id) |
api.Projects.UpdateReportForwardingAsync(id, payload) |
lettermint.Projects.ReportForwarding.UpdateAsync(id, payload) |
api.Projects.DeleteReportForwardingAsync(id) |
lettermint.Projects.ReportForwarding.DeleteAsync(id) |
api.Projects.VerifyReportForwardingAsync(id, payload) |
lettermint.Projects.ReportForwarding.VerifyAsync(id, payload) |
api.Projects.ResendReportForwardingCodeAsync(id) |
lettermint.Projects.ReportForwarding.ResendCodeAsync(id) |
api.Routes.ListAsync(projectId, options) |
lettermint.Routes.ListAsync(projectId, query?), lettermint.Routes.IterateAsync(projectId, query?) |
api.Routes.CreateAsync(projectId, payload) |
lettermint.Routes.CreateAsync(projectId, payload) |
api.Routes.RetrieveAsync(id) |
lettermint.Routes.RetrieveAsync(id, query?) |
api.Routes.UpdateAsync(id, payload) |
lettermint.Routes.UpdateAsync(id, payload) |
api.Routes.DeleteAsync(id) |
lettermint.Routes.DeleteAsync(id) |
api.Routes.VerifyInboundDomainAsync(id) |
lettermint.Routes.VerifyInboundDomainAsync(id) |
api.Stats.RetrieveAsync(options with from/to query) |
lettermint.Stats.RetrieveAsync(new GetStatsQuery { From, To, ProjectId?, IncludeMachine? }) |
api.Suppressions.ListAsync(options) |
lettermint.Suppressions.ListAsync(query?), lettermint.Suppressions.IterateAsync(query?) |
api.Suppressions.CreateAsync(payload) |
lettermint.Suppressions.CreateAsync(payload) |
api.Suppressions.DeleteAsync(id) |
lettermint.Suppressions.DeleteAsync(id) |
api.Team.RetrieveAsync() |
lettermint.Team.RetrieveAsync(query?) (Include = [GetTeamQueryIncludeItem.Features]) |
api.Team.UpdateAsync(payload) |
lettermint.Team.UpdateAsync(payload) |
api.Team.UsageAsync() |
lettermint.Team.UsageAsync() |
api.Team.RolesAsync() |
lettermint.Team.RolesAsync() |
api.Team.MembersAsync(options) |
lettermint.Team.Members.ListAsync(query?), lettermint.Team.Members.IterateAsync(query?) |
api.Team.RetrieveMemberAsync(userId) |
lettermint.Team.Members.RetrieveAsync(userId) |
api.Team.UpdateMemberAssignmentAsync(userId, payload) |
lettermint.Team.Members.UpdateAssignmentAsync(userId, payload) |
api.Webhooks.ListAsync(options) |
lettermint.Webhooks.ListAsync(query?), lettermint.Webhooks.IterateAsync(query?) |
api.Webhooks.CreateAsync(payload) |
lettermint.Webhooks.CreateAsync(payload) |
api.Webhooks.RetrieveAsync(id) |
lettermint.Webhooks.RetrieveAsync(id) |
api.Webhooks.UpdateAsync(id, payload) |
lettermint.Webhooks.UpdateAsync(id, payload) |
api.Webhooks.DeleteAsync(id) |
lettermint.Webhooks.DeleteAsync(id) |
api.Webhooks.TestAsync(id) |
lettermint.Webhooks.TestAsync(id) |
api.Webhooks.RegenerateSecretAsync(id) |
lettermint.Webhooks.RegenerateSecretAsync(id) |
api.Webhooks.DeliveriesAsync(id, options) |
lettermint.Webhooks.Deliveries.ListAsync(id, query?), lettermint.Webhooks.Deliveries.IterateAsync(id, query?) |
api.Webhooks.ShowDeliveryAsync(id, deliveryId) |
lettermint.Webhooks.Deliveries.RetrieveAsync(id, deliveryId) |
Every method takes RequestOptions? options (now with a per-call Timeout) and a CancellationToken last, as before. RequestOptions.Query and RequestOptions.Headers are removed; RequestOptions.IdempotencyKey moved to IdempotentRequestOptions, which Emails.SendAsync, Emails.SendBatchAsync and Messages.ProcessAsync take.
1.x RequestOptions.Query |
2.0 query record |
|---|---|
["page[size]"] = "30", ["page[cursor]"] = c |
new ListDomainsQuery { PageSize = 30, PageCursor = c } |
["filter[status]"] = "verified" |
FilterStatus = DomainStatus.Verified |
["sort"] = "-created_at,domain" |
Sort = [ListDomainsQuerySortItem.CreatedAtDescending, ListDomainsQuerySortItem.Domain] |
["filter[tags][0][name]"] = "a", ["filter[tags][0][value]"] = "b" |
Filter = new ListMessagesQueryFilter { Tags = [new() { Name = "a", Value = "b" }] } |
["filter[enabled]"] = "true" |
FilterEnabled = true (sent as 1) |
webhooks: ["cursor"] = c |
Cursor = c (these lists use cursor, not page[cursor]) |
Properties are named after the wire names (page[size] → PageSize, filter[from_date] → FilterFromDate).
IDs are still URL-encoded. An empty or null ID, . or .. now throws LettermintConfigException (1.x: ArgumentException) before the request.
List responses are CursorPage<T>: every named page type (ListDomainsResponse, ListMessagesResponse, …) derives from CursorPage<T> with Data, NextCursor, PerPage and the other cursor fields. 1.x documented a Meta object on message lists; read page.NextCursor, or use IterateAsync().
Suppressions.DeleteAsyncreturnsDeleteSuppressionResponse(one record for HTTP 200 and 202). CheckStatus:review_ticket_createdandreview_ticket_existsmean the suppression was not removed yet.Emails.SendAsyncreturnsSendMailResponse, withStatus(PendingorScheduled) andScheduledAt.Projects.ReportForwarding.DeleteAsyncreturnsTask(HTTP 204), as before.- Text endpoints (
SourceAsync,HtmlAsync,TextAsync) still return the raw string; both ping methods still return the trimmedpong.
LettermintException stays the base class. LettermintApiException stays the class of HTTP errors, with subclasses per status. Timeouts, network failures and decoding failures, which were all a plain LettermintException in 1.x, have their own classes.
| Situation | 1.x | 2.0 |
|---|---|---|
| HTTP 400 and other 4xx | LettermintApiException (StatusCode, ResponseBody) |
LettermintApiException (StatusCode, Code, Message, Details, Body) |
| HTTP 401 | LettermintApiException |
AuthenticationException |
| HTTP 403 | LettermintApiException |
PermissionException |
| HTTP 404 | LettermintApiException |
NotFoundException |
| HTTP 409 | LettermintApiException |
ConflictException |
| HTTP 422 | LettermintApiException |
ValidationException (Errors) |
| HTTP 429 | LettermintApiException |
RateLimitException (RetryAfter) |
| HTTP 5xx | LettermintApiException |
ServerException |
| Empty or invalid JSON body, HTML error page | LettermintException ("invalid JSON response") or LettermintApiException with the HTML |
UnexpectedResponseException (StatusCode, BodyExcerpt) |
| Redirect (3xx) | LettermintApiException (the default handler did not follow it) |
RedirectException (StatusCode); also thrown when a caller-supplied HttpClient followed one |
| Timeout | LettermintException ("The request timed out.") |
LettermintTimeoutException (Timeout); covers headers and body |
| Network failure | LettermintException ("The HTTP request failed.") |
ConnectionException (InnerException) |
| Caller cancellation | OperationCanceledException |
OperationCanceledException, unchanged |
| An unknown enum value in a response | LettermintException (JSON error "Unknown API enum value.") |
Decodes; the value keeps its raw string |
| Invalid tags | ArgumentException |
LettermintValidationException (Field) |
| Missing or wrong token, bad option, bad ID | ArgumentException |
LettermintConfigException |
| Webhook verification | WebhookVerificationException |
WebhookVerificationException (Reason, ReasonCode) |
Property changes: ResponseBody (a token-redacted string) is replaced by Body (the decoded JSON as a JsonElement?), plus Code, Details and the API's Message (1.x: Lettermint returned HTTP <status>.).
// 1.x
catch (LettermintApiException error) when (error.StatusCode == HttpStatusCode.TooManyRequests)
{
Console.WriteLine(error.ResponseBody);
}
// 2.0
catch (RateLimitException error)
{
await Task.Delay(error.RetryAfter ?? TimeSpan.FromSeconds(1));
}
catch (ValidationException error)
{
Console.WriteLine($"{error.Code} {error.Message}");
}The SDK does not retry requests. Pass an idempotency key when you retry a send.
Webhook is an instance class now. Verify takes the raw body and the request headers, requires both X-Lettermint-Signature and X-Lettermint-Delivery, and returns a typed WebhookPayload instead of a JsonElement. The old signature-only check is VerifySignature.
// 1.x
JsonElement payload = Webhook.Verify(rawBody, signatureHeader, secret, toleranceSeconds: 300);
// 2.0
var webhook = new Webhook(secret); // keep it, for example as a singleton
WebhookPayload payload = webhook.Verify(rawBody, request.Headers); // IHeaderDictionary, HttpHeaders, dictionaries
WebhookPayload payload2 = webhook.VerifySignature(rawBody, signatureHeader, deliveryHeader);- The body may be a
stringor bytes (byte[],ReadOnlySpan<byte>). - The tolerance is a
TimeSpan(default 300 seconds) and applies in both directions.TimeSpan.Zeroaccepts only the current second; in 1.x,0disabled the check. - The clock is the third constructor argument (
TimeProvider), as before. payload.Eventis aWebhookEvent,payload.DataaJsonElement; other fields are inAdditionalProperties, andGetData<T>()deserializesData.WebhookVerificationException.Reasontells why verification failed:SignatureHeaderMissing,SignatureHeaderMalformed,DeliveryHeaderMissing,DeliveryTimestampMismatch,TimestampOutOfTolerance,SignatureMismatch,BodyInvalidorPayloadInvalid(ReasonCode: the snake_case codes of the other SDKs).- An empty secret or a negative tolerance throws
LettermintConfigException(1.x:WebhookVerificationExceptionorArgumentOutOfRangeException).
The types are generated from the API specification of lettermint#2582 and use its names. They stay in the Lettermint.Models namespace. Types that are not listed below keep their name. Some shapes also changed:
- Generated types are
sealed partial recordtypes withinitproperties instead of classes withget; set;properties. Required fields use therequiredmodifier, so object initializers must set them. Unknown fields are still kept inAdditionalProperties, which is anIDictionary<string, JsonElement>?. - Lists are
IReadOnlyList<T>(1.x:List<T>), maps areIReadOnlyDictionary<string, T>(1.x:Dictionary<string, T>). Collection expressions (To = ["a@example.com"]) work for both. - Enums are open
readonly structtypes instead of C#enumtypes. Static values keep their names (MessageStatus.Delivered,TlsPolicy.Enforced), but they are not constants:case MessageStatus.Delivered:no longer compiles. Use==,if,switchonToString(), orwhenclauses, and handle values the API adds later.RecordType.TXT,RecordType.CNAMEandRecordType.MXareRecordType.Txt,RecordType.CnameandRecordType.Mx. ApiModel(the base class withToJson()) is removed; serialize withSystem.Text.Jsondirectly.OptionalNullable<T>isOptional<T?>;OptionalNullable<T>.Nullisnull.- One type for tags in responses (
MessageTag) and one for tags you send (MessageTagInput). SendMailResponseis one record for pending and scheduled sends.ToString()of a record that holds a secret returned by the API (ProjectCreatedData.ApiToken,RotateProjectTokenResponse.NewToken,WebhookSecretData.Secret,WebhookBasicAuthData.Password) shows[redacted]for it; the property itself still returns the value.
| 1.x | 2.0 |
|---|---|
CancelScheduledMessageResponse |
ScheduledMessage |
DomainDestroyResponse |
MessageResponse |
DomainIndexResponse |
ListDomainsResponse |
DomainUpdateProjectsResponse |
DomainMutationResponse |
DomainVerifyDnsRecordsResponse |
DnsVerificationSuccessResponse |
DomainVerifyDnsRecordsResponseRecommendedFailedRecordsItem |
DnsVerificationFailedRecord |
DomainVerifySpecificDnsRecordResponse |
MessageResponse |
MessageDataTagsItem |
MessageTag |
MessageEventDataTagsItem |
MessageTag |
MessageEventsResponse |
ListMessageEventsResponse |
MessageIndexResponse |
ListMessagesResponse |
MessageListDataTagsItem |
MessageTag |
ProcessInboundMessageResponseData |
ProcessInboundMessageResult |
ProjectDestroyResponse |
MessageResponse |
ProjectIndexResponse |
ListProjectsResponse |
ProjectRotateTokenResponse |
RotateProjectTokenResponse |
ProjectStoreResponse |
ProjectCreatedData |
ProjectUpdateResponse |
ProjectMutationResponse |
RescheduleMessageResponse |
ScheduledMessage |
RouteDestroyResponse |
MessageResponse |
RouteIndexResponse |
ListRoutesResponse |
RouteStoreResponse |
RouteMutationResponse |
RouteUpdateResponse |
RouteMutationResponse |
RouteVerifyInboundDomainResponse |
InboundDomainVerificationResponse |
RouteVerifyInboundDomainResponseData |
InboundDomainVerification |
SendBatchEmailResponseItem |
SendMailResponse |
SendBatchMailRequestItem |
SendMailRequest |
SendBatchMailRequestItemAttachmentsItem |
MessageAttachmentInput |
SendBatchMailRequestItemSettings |
SendMailRequestSettings |
SendBatchMailRequestItemTagsItem |
MessageTagInput |
SendEmailResponse |
SendMailResponse |
SendMailRequestAttachmentsItem |
MessageAttachmentInput |
SendMailRequestTagsItem |
MessageTagInput |
SuppressionDestroyResponse |
DeleteSuppressionResponse |
SuppressionIndexResponse |
ListSuppressionsResponse |
SuppressionStoreResponseData |
SuppressionStoreResult |
TeamMembersResponse |
ListTeamMembersResponse |
TeamRolesResponse |
TeamRoleListResponse |
TeamUpdateResponse |
TeamMutationResponse |
V1AnalyticsRequest |
AnalyticsQuery |
V1AnalyticsRequestCompare |
AnalyticsComparison |
V1AnalyticsRequestFiltersItem |
AnalyticsFilter |
V1AnalyticsRequestFiltersItemOperator |
AnalyticsFilterOperator |
V1AnalyticsRequestIncludeItem |
AnalyticsSection |
V1AnalyticsRequestInterval |
AnalyticsInterval |
V1AnalyticsRequestMetricsItem |
AnalyticsMetric |
V1AnalyticsRequestSort |
AnalyticsSort |
V1AnalyticsRequestSortDirection |
AnalyticsSortDirection |
V1AnalyticsRequestSortMetric |
AnalyticsMetric |
V1AnalyticsResponse |
AnalyticsResponse |
V1AnalyticsResponseData |
AnalyticsResults |
V1AnalyticsResponseDataBreakdownItem |
AnalyticsBreakdownRow |
V1AnalyticsResponseDataBreakdownItemChangeValue |
AnalyticsMetricChange |
V1AnalyticsResponseDataBreakdownItemMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataBreakdownItemPrevious |
AnalyticsComparisonValues |
V1AnalyticsResponseDataBreakdownItemPreviousMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataBreakdownItemPreviousRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemPreviousRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataBreakdownItemRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItem |
AnalyticsTimeSeriesPoint |
V1AnalyticsResponseDataBreakdownItemTrendItemChangeValue |
AnalyticsMetricChange |
V1AnalyticsResponseDataBreakdownItemTrendItemMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataBreakdownItemTrendItemPrevious |
AnalyticsComparisonValues |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemPreviousRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataBreakdownItemTrendItemRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummary |
AnalyticsSummary |
V1AnalyticsResponseDataSummaryChangeValue |
AnalyticsMetricChange |
V1AnalyticsResponseDataSummaryMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataSummaryPrevious |
AnalyticsComparisonValues |
V1AnalyticsResponseDataSummaryPreviousMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataSummaryPreviousRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataSummaryPreviousRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryPreviousRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryPreviousRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryPreviousRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryPreviousRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryPreviousRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryPreviousRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataSummaryRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataSummaryRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItem |
AnalyticsTimeSeriesPoint |
V1AnalyticsResponseDataTimeSeriesItemChangeValue |
AnalyticsMetricChange |
V1AnalyticsResponseDataTimeSeriesItemMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataTimeSeriesItemPrevious |
AnalyticsComparisonValues |
V1AnalyticsResponseDataTimeSeriesItemPreviousMetrics |
AnalyticsMetricValues |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemPreviousRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBases |
AnalyticsRateBases |
V1AnalyticsResponseDataTimeSeriesItemRateBasesBounceRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBasesComplaintRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBasesDeferralRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBasesDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBasesEffectiveDeliveryRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBasesHumanClickRate |
AnalyticsRateBase |
V1AnalyticsResponseDataTimeSeriesItemRateBasesHumanOpenRate |
AnalyticsRateBase |
V1AnalyticsResponseMeta |
AnalyticsMeta |
V1AnalyticsResponseMetaAlignment |
AnalyticsMetaAlignment |
V1AnalyticsResponseMetaCollectionCompleteness |
AnalyticsMetaCollectionCompleteness |
V1AnalyticsResponseMetaComparison |
AnalyticsMetaComparison |
V1AnalyticsResponseMetaInterval |
AnalyticsInterval |
V1AnalyticsResponseMetaTimeBasis |
AnalyticsMetaTimeBasis |
V1AnalyticsResponsePagination |
AnalyticsPagination |
V1BlockedFileTypesResponse |
BlockedFileTypes |
WebhookDeliveriesResponse |
ListWebhookDeliveriesResponse |
WebhookDestroyResponse |
MessageResponse |
WebhookIndexResponse |
ListWebhooksResponse |
WebhookRegenerateSecretResponse |
WebhookSecretResponse |
WebhookStoreResponse |
WebhookSecretResponse |
WebhookTestResponse |
TestWebhookResponse |
WebhookUpdateResponse |
WebhookMutationResponse |
lettermint#2582 removed these schemas from the API specification:
| 1.x | 2.0 |
|---|---|
AnalyticsResponseData |
Removed. Use AnalyticsResponse (Data is AnalyticsResults). |
StatsRequestData |
Removed. Use GetStatsQuery, the parameters of Stats.RetrieveAsync(). |
MessageIndexResponseMeta |
Removed. Message lists are flat CursorPage<MessageListData> pages. |
MessageEventsResponseMeta |
Removed. Event lists are flat CursorPage<MessageEventData> pages. |
SuppressionStoreResponseMessage1 |
Removed; not exported by 1.x. SuppressionStoreResponse.Message is a string. |
| 1.x | 2.0 |
|---|---|
LettermintClient.Email(token, options) (static) |
new LettermintClient(new LettermintOptions { SendingToken = token, … }).Emails |
LettermintClient.Api(token, options) (static) |
new LettermintClient(new LettermintOptions { TeamToken = token, … }) |
EmailClient |
Emails (lettermint.Emails) |
ApiClient |
LettermintClient |
DomainsEndpoint, MessagesEndpoint, ProjectsEndpoint, RoutesEndpoint, StatsEndpoint, SuppressionsEndpoint, TeamEndpoint, WebhooksEndpoint |
Domains, Messages, Projects (with ReportForwarding), Routes, Stats, Suppressions, Team (with TeamMembers), Webhooks (with WebhookDeliveries); use the properties of LettermintClient |
ClientOptions |
LettermintOptions |
RequestOptions (Query, Headers, IdempotencyKey) |
RequestOptions (Timeout), IdempotentRequestOptions (IdempotencyKey) and typed query records |
EmailBuilder (mutable) |
EmailBuilder (immutable), from lettermint.Emails.Compose() |
MessageTag (hand-written) |
MessageTagInput |
OptionalNullable<T>, OptionalNullableConverterFactory |
Optional<T> |
Webhook.Verify(payload, signature, secret, …) (static) |
new Webhook(secret, tolerance?, timeProvider?).Verify(body, headers) or .VerifySignature(body, signature, delivery?) |
ApiModel |
Removed. Records have AdditionalProperties; serialize with System.Text.Json. |
LettermintJson |
Removed (internal). Use System.Text.Json with default options. |
JsonUnion<TObject, TArray>, JsonUnionConverterFactory |
Removed; no response uses them any more. |
WireEnumConverter<T>, CollectionResponseConverter<T>, ApiDictionaryConverterFactory |
Removed (internal converters). |
ApiOperationAttribute |
Removed. |
JsonElement payload overloads of every method |
Removed. Use the request records; put unknown fields in AdditionalProperties. |